From f74185a5dfe2ec0bba40d688721fc8d83bef11ce Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 10 Aug 2023 11:20:04 +0200 Subject: [PATCH 001/278] feat: introduce flowpilot --- backend/flowpilot/builder.go | 71 ++++++ backend/flowpilot/context.go | 236 ++++++++++++++++++ backend/flowpilot/context_flow.go | 94 +++++++ backend/flowpilot/context_methodExecution.go | 146 +++++++++++ .../flowpilot/context_methodInitialization.go | 25 ++ backend/flowpilot/db.go | 179 +++++++++++++ backend/flowpilot/errors.go | 23 ++ backend/flowpilot/flow.go | 175 +++++++++++++ backend/flowpilot/input.go | 228 +++++++++++++++++ backend/flowpilot/input_schema.go | 157 ++++++++++++ backend/flowpilot/jsonmanager/manager.go | 84 +++++++ backend/flowpilot/response.go | 119 +++++++++ backend/flowpilot/utils/param.go | 46 ++++ 13 files changed, 1583 insertions(+) create mode 100644 backend/flowpilot/builder.go create mode 100644 backend/flowpilot/context.go create mode 100644 backend/flowpilot/context_flow.go create mode 100644 backend/flowpilot/context_methodExecution.go create mode 100644 backend/flowpilot/context_methodInitialization.go create mode 100644 backend/flowpilot/db.go create mode 100644 backend/flowpilot/errors.go create mode 100644 backend/flowpilot/flow.go create mode 100644 backend/flowpilot/input.go create mode 100644 backend/flowpilot/input_schema.go create mode 100644 backend/flowpilot/jsonmanager/manager.go create mode 100644 backend/flowpilot/response.go create mode 100644 backend/flowpilot/utils/param.go diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go new file mode 100644 index 000000000..8dca8e084 --- /dev/null +++ b/backend/flowpilot/builder.go @@ -0,0 +1,71 @@ +package flowpilot + +import ( + "time" +) + +// FlowBuilder is a builder struct for creating a new Flow. +type FlowBuilder struct { + path string + ttl time.Duration + initialState StateName + errorState StateName + endState StateName + flow StateTransitions +} + +// NewFlow creates a new FlowBuilder that builds a new flow available under the specified path. +func NewFlow(path string) *FlowBuilder { + return &FlowBuilder{ + path: path, + flow: make(StateTransitions), + } +} + +// TTL sets the time-to-live (TTL) for the flow. +func (fb *FlowBuilder) TTL(ttl time.Duration) *FlowBuilder { + fb.ttl = ttl + return fb +} + +// State adds a new state transition to the FlowBuilder. +func (fb *FlowBuilder) State(state StateName, mList ...Method) *FlowBuilder { + var transitions Transitions + for _, m := range mList { + transitions = append(transitions, Transition{Method: m}) + } + fb.flow[state] = transitions + return fb +} + +// FixedStates sets the initial and final states of the flow. +func (fb *FlowBuilder) FixedStates(initialState, errorState, finalState StateName) *FlowBuilder { + fb.initialState = initialState + fb.errorState = errorState + fb.endState = finalState + return fb +} + +// Build constructs and returns the Flow object. +func (fb *FlowBuilder) Build() Flow { + return Flow{ + Path: fb.path, + Flow: fb.flow, + InitialState: fb.initialState, + ErrorState: fb.errorState, + EndState: fb.endState, + TTL: fb.ttl, + } +} + +// TransitionBuilder is a builder struct for creating a new Transition. +type TransitionBuilder struct { + method Method +} + +// Build constructs and returns the Transition object. +func (tb *TransitionBuilder) Build() *Transition { + return &Transition{ + Method: tb.method, + } +} diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go new file mode 100644 index 000000000..83b1adeae --- /dev/null +++ b/backend/flowpilot/context.go @@ -0,0 +1,236 @@ +package flowpilot + +import ( + "database/sql" + "errors" + "fmt" + "github.com/gofrs/uuid" + "hanko_flowsc/flowpilot/jsonmanager" + "hanko_flowsc/flowpilot/utils" + "time" +) + +// flowContext represents the basic context for a Flow. +type flowContext interface { + // GetFlowID returns the unique ID of the current Flow. + GetFlowID() uuid.UUID + // GetPath returns the current path within the Flow. + GetPath() string + // Payload returns the JSONManager for accessing payload data. + Payload() jsonmanager.JSONManager + // Stash returns the JSONManager for accessing stash data. + Stash() jsonmanager.JSONManager + // Flash returns the JSONManager for accessing flash data. + Flash() jsonmanager.JSONManager + // GetInitialState returns the initial state of the Flow. + GetInitialState() StateName + // GetCurrentState returns the current state of the Flow. + GetCurrentState() StateName + // GetPreviousState returns the previous state of the Flow. + GetPreviousState() *StateName + // GetErrorState returns the designated error state of the Flow. + GetErrorState() StateName + // GetEndState returns the final state of the Flow. + GetEndState() StateName + // StateExists checks if a given state exists within the Flow. + StateExists(stateName StateName) bool +} + +// methodInitializationContext represents the basic context for a Flow method's initialization. +type methodInitializationContext interface { + // AddInputs adds input parameters to the schema. + AddInputs(inputList ...*DefaultInput) *DefaultSchema + // Stash returns the ReadOnlyJSONManager for accessing stash data. + Stash() jsonmanager.ReadOnlyJSONManager + // SuspendMethod suspends the current method's execution. + SuspendMethod() +} + +// methodExecutionContext represents the context for a method execution. +type methodExecutionContext interface { + // Input returns the ReadOnlyJSONManager for accessing input data. + Input() jsonmanager.ReadOnlyJSONManager + // Schema returns the MethodExecutionSchema for the method. + Schema() MethodExecutionSchema + + // TODO: FetchMethodInput (for a method name) is maybe useless and can be removed or replaced. + + // FetchMethodInput fetches input data for a specific method. + FetchMethodInput(methodName MethodName) (jsonmanager.ReadOnlyJSONManager, error) + // ValidateInputData validates the input data against the schema. + ValidateInputData() bool + + // TODO: CopyInputsToStash can maybe removed or replaced with an option you can set via the input options. + + // CopyInputsToStash copies specified inputs to the stash. + CopyInputsToStash(inputNames ...string) error +} + +// methodExecutionContinuationContext represents the context within a method continuation. +type methodExecutionContinuationContext interface { + flowContext + methodExecutionContext + // ContinueFlow continues the Flow execution to the specified next state. + ContinueFlow(nextState StateName) error + // ContinueFlowWithError continues the Flow execution to the specified next state with an error. + ContinueFlowWithError(nextState StateName, errType *ErrorType) error + + // TODO: Implement a function to step back to the previous state (while skipping self-transitions and recalling preserved data). +} + +// InitializationContext is a shorthand for methodInitializationContext within flow methods. +type InitializationContext interface { + methodInitializationContext +} + +// ExecutionContext is a shorthand for methodExecutionContinuationContext within flow methods. +type ExecutionContext interface { + methodExecutionContinuationContext +} + +// TODO: The following interfaces are meant for a plugin system. #tbd + +// PluginBeforeMethodExecutionContext represents the context for a plugin before a method execution. +type PluginBeforeMethodExecutionContext interface { + methodExecutionContinuationContext +} + +// PluginAfterMethodExecutionContext represents the context for a plugin after a method execution. +type PluginAfterMethodExecutionContext interface { + methodExecutionContext +} + +// createAndInitializeFlow initializes the Flow and returns a flow Response. +func createAndInitializeFlow(db FlowDB, flow Flow) (*Response, error) { + // Wrap the provided FlowDB with additional functionality. + dbw := wrapDB(db) + // Calculate the expiration time for the Flow. + expiresAt := time.Now().Add(flow.TTL).UTC() + + // Initialize JSONManagers for stash, payload, and flash data. + + // TODO: Consider implementing types for stash, payload, and flash that extend "jsonmanager.NewJSONManager()". + // This could enhance the code structure and provide clearer interfaces for handling these data structures. + + stash := jsonmanager.NewJSONManager() + payload := jsonmanager.NewJSONManager() + flash := jsonmanager.NewJSONManager() + + // Create a new Flow model with the provided parameters. + p := flowCreationParam{currentState: flow.InitialState, expiresAt: expiresAt} + flowModel, err := dbw.CreateFlowWithParam(p) + if err != nil { + return nil, fmt.Errorf("failed to create Flow: %w", err) + } + + // Create a defaultFlowContext instance. + fc := defaultFlowContext{ + flow: flow, + dbw: dbw, + flowModel: *flowModel, + stash: stash, + payload: payload, + flash: flash, + } + + // Generate a response based on the execution result. + er := executionResult{nextState: flowModel.CurrentState} + return er.generateResponse(fc) +} + +// executeFlowMethod processes the Flow and returns a Response. +func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Response, error) { + // Parse the action parameter to get the method name and Flow ID. + action, err := utils.ParseActionParam(options.action) + if err != nil { + return nil, fmt.Errorf("failed to parse action param: %w", err) + } + + // Retrieve the Flow model from the database using the Flow ID. + flowModel, err := db.GetFlow(action.FlowID) + if err != nil { + if err == sql.ErrNoRows { + return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + } + return nil, fmt.Errorf("failed to get Flow: %w", err) + } + + // Check if the Flow has expired. + if time.Now().After(flowModel.ExpiresAt) { + return &Response{State: flow.ErrorState, Error: FlowExpiredError}, nil + } + + // Parse stash data from the Flow model. + stash, err := jsonmanager.NewJSONManagerFromString(flowModel.StashData) + if err != nil { + return nil, fmt.Errorf("failed to parse stash from flowModel: %w", err) + } + + // Initialize JSONManagers for payload and flash data. + payload := jsonmanager.NewJSONManager() + flash := jsonmanager.NewJSONManager() + + // Create a defaultFlowContext instance. + fc := defaultFlowContext{ + flow: flow, + dbw: wrapDB(db), + flowModel: *flowModel, + stash: stash, + payload: payload, + flash: flash, + } + + // Get the available transitions for the current state. + transitions := fc.getCurrentTransitions() + if transitions == nil { + return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + } + + // Parse raw input data into JSONManager. + raw := options.inputData.getJSONStringOrDefault() + inputJSON, err := jsonmanager.NewJSONManagerFromString(raw) + if err != nil { + return nil, fmt.Errorf("failed to parse input data: %w", err) + } + + // Create a MethodName from the parsed action method name. + methodName := MethodName(action.MethodName) + // Get the method associated with the action method name. + method, err := transitions.getMethod(methodName) + if err != nil { + return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + } + + // Initialize the schema and method context for method execution. + schema := DefaultSchema{} + mic := defaultMethodInitializationContext{schema: &schema, stash: stash} + method.Initialize(&mic) + + // Check if the method is suspended. + if mic.isSuspended { + return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + } + + // Create a methodExecutionContext instance for method execution. + mec := defaultMethodExecutionContext{ + schema: &schema, + methodName: methodName, + input: inputJSON, + defaultFlowContext: fc, + } + + // Execute the method and handle any errors. + err = method.Execute(&mec) + if err != nil { + return nil, fmt.Errorf("the method failed to handle the request: %w", err) + } + + // Ensure that the method has set a result object. + if mec.methodResult == nil { + return nil, errors.New("the method has not set a result object") + } + + // Generate a response based on the execution result. + er := *mec.methodResult + return er.generateResponse(fc) +} diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go new file mode 100644 index 000000000..7c0429a3b --- /dev/null +++ b/backend/flowpilot/context_flow.go @@ -0,0 +1,94 @@ +package flowpilot + +import ( + "fmt" + "github.com/gofrs/uuid" + "hanko_flowsc/flowpilot/jsonmanager" +) + +// defaultFlowContext is the default implementation of the flowContext interface. +type defaultFlowContext struct { + payload jsonmanager.JSONManager // JSONManager for payload data. + stash jsonmanager.JSONManager // JSONManager for stash data. + flash jsonmanager.JSONManager // JSONManager for flash data. + flow Flow // The associated Flow instance. + dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. + flowModel FlowModel // The current FlowModel. +} + +// GetFlowID returns the unique ID of the current Flow. +func (fc *defaultFlowContext) GetFlowID() uuid.UUID { + return fc.flowModel.ID +} + +// GetPath returns the current path within the Flow. +func (fc *defaultFlowContext) GetPath() string { + return fc.flow.Path +} + +// GetInitialState returns the initial state of the Flow. +func (fc *defaultFlowContext) GetInitialState() StateName { + return fc.flow.InitialState +} + +// GetCurrentState returns the current state of the Flow. +func (fc *defaultFlowContext) GetCurrentState() StateName { + return fc.flowModel.CurrentState +} + +// GetPreviousState returns a pointer to the previous state of the Flow. +func (fc *defaultFlowContext) GetPreviousState() *StateName { + return fc.flowModel.PreviousState +} + +// GetErrorState returns the designated error state of the Flow. +func (fc *defaultFlowContext) GetErrorState() StateName { + return fc.flow.ErrorState +} + +// GetEndState returns the final state of the Flow. +func (fc *defaultFlowContext) GetEndState() StateName { + return fc.flow.EndState +} + +// Stash returns the JSONManager for accessing stash data. +func (fc *defaultFlowContext) Stash() jsonmanager.JSONManager { + return fc.stash +} + +// Flash returns the JSONManager for accessing flash data. +func (fc *defaultFlowContext) Flash() jsonmanager.JSONManager { + return fc.flash +} + +// StateExists checks if a given state exists within the Flow. +func (fc *defaultFlowContext) StateExists(stateName StateName) bool { + return fc.flow.stateExists(stateName) +} + +// FetchMethodInput fetches input data for a specific method. +func (fc *defaultFlowContext) FetchMethodInput(methodName MethodName) (jsonmanager.ReadOnlyJSONManager, error) { + // Find the last Transition with the specified method from the database wrapper. + t, err := fc.dbw.FindLastTransitionWithMethod(fc.flowModel.ID, methodName) + if err != nil { + return nil, fmt.Errorf("failed to get last Transition from dbw: %w", err) + } + + // If no Transition is found, return an empty JSONManager. + if t == nil { + return jsonmanager.NewJSONManager(), nil + } + + // Parse input data from the Transition. + inputData, err := jsonmanager.NewJSONManagerFromString(t.InputData) + if err != nil { + return nil, fmt.Errorf("failed to decode Transition data: %w", err) + } + + return inputData, nil +} + +// getCurrentTransitions retrieves the Transitions for the current state. +func (fc *defaultFlowContext) getCurrentTransitions() *Transitions { + return fc.flow.getTransitionsForState(fc.flowModel.CurrentState) +} diff --git a/backend/flowpilot/context_methodExecution.go b/backend/flowpilot/context_methodExecution.go new file mode 100644 index 000000000..5ab38376d --- /dev/null +++ b/backend/flowpilot/context_methodExecution.go @@ -0,0 +1,146 @@ +package flowpilot + +import ( + "errors" + "fmt" + "hanko_flowsc/flowpilot/jsonmanager" +) + +// defaultMethodExecutionContext is the default implementation of the methodExecutionContext interface. +type defaultMethodExecutionContext struct { + methodName MethodName // Name of the method being executed. + input jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing input data. + schema MethodExecutionSchema // Schema for the method execution. + methodResult *executionResult // Result of the method execution. + + defaultFlowContext // Embedding the defaultFlowContext for common context fields. +} + +// saveNextState updates the Flow's state and saves Transition data after method execution. +func (mec *defaultMethodExecutionContext) saveNextState(executionResult executionResult) error { + currentState := mec.flowModel.CurrentState + previousState := mec.flowModel.PreviousState + + // Update the previous state only if the next state is different from the current state. + if executionResult.nextState != currentState { + previousState = ¤tState + } + + completed := executionResult.nextState == mec.flow.EndState + newVersion := mec.flowModel.Version + 1 + + // Prepare parameters for updating the Flow in the database. + flowUpdateParam := flowUpdateParam{ + flowID: mec.flowModel.ID, + nextState: executionResult.nextState, + previousState: previousState, + stashData: mec.stash.String(), + version: newVersion, + completed: completed, + expiresAt: mec.flowModel.ExpiresAt, + createdAt: mec.flowModel.CreatedAt, + } + + // Update the Flow model in the database. + flowModel, err := mec.dbw.UpdateFlowWithParam(flowUpdateParam) + if err != nil { + return fmt.Errorf("failed to store updated flow: %w", err) + } + + mec.flowModel = *flowModel + + // Get transition data from the response schema for recording. + inputData, err := mec.schema.toResponseSchema().getTransitionData(mec.input) + if err != nil { + return fmt.Errorf("failed to get data from response schema: %w", err) + } + + // Prepare parameters for creating a new Transition in the database. + transitionCreationParam := transitionCreationParam{ + flowID: mec.flowModel.ID, + methodName: mec.methodName, + fromState: currentState, + toState: executionResult.nextState, + inputData: inputData.String(), + errType: executionResult.errType, + } + + // Create a new Transition in the database. + _, err = mec.dbw.CreateTransitionWithParam(transitionCreationParam) + if err != nil { + return fmt.Errorf("failed to store a new transition: %w", err) + } + + return nil +} + +// continueFlow continues the Flow execution to the specified nextState with an optional error type. +func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, errType *ErrorType) error { + // Check if the specified nextState is valid. + if exists := mec.flow.stateExists(nextState); !exists { + return errors.New("the execution result contains an invalid state") + } + + // Prepare an executionResult for continuing the Flow. + methodResult := executionResult{ + nextState: nextState, + errType: errType, + methodExecutionResult: &methodExecutionResult{ + methodName: mec.methodName, + schema: mec.schema.toResponseSchema(), + inputData: mec.input, + }, + } + + // Save the next state and transition data. + err := mec.saveNextState(methodResult) + if err != nil { + return fmt.Errorf("failed to save the transition data: %w", err) + } + + mec.methodResult = &methodResult + + return nil +} + +// Input returns the ReadOnlyJSONManager for accessing input data. +func (mec *defaultMethodExecutionContext) Input() jsonmanager.ReadOnlyJSONManager { + return mec.input +} + +// Payload returns the JSONManager for accessing payload data. +func (mec *defaultMethodExecutionContext) Payload() jsonmanager.JSONManager { + return mec.payload +} + +// Schema returns the MethodExecutionSchema for the method. +func (mec *defaultMethodExecutionContext) Schema() MethodExecutionSchema { + return mec.schema +} + +// CopyInputsToStash copies specified inputs to the stash. +func (mec *defaultMethodExecutionContext) CopyInputsToStash(inputNames ...string) error { + for _, inputName := range inputNames { + // Copy input values to the stash. + err := mec.stash.Set(inputName, mec.input.Get(inputName).Value()) + if err != nil { + return err + } + } + return nil +} + +// ValidateInputData validates the input data against the schema. +func (mec *defaultMethodExecutionContext) ValidateInputData() bool { + return mec.Schema().toResponseSchema().validateInputData(mec.input, mec.stash) +} + +// ContinueFlow continues the Flow execution to the specified nextState. +func (mec *defaultMethodExecutionContext) ContinueFlow(nextState StateName) error { + return mec.continueFlow(nextState, nil) +} + +// ContinueFlowWithError continues the Flow execution to the specified nextState with an error type. +func (mec *defaultMethodExecutionContext) ContinueFlowWithError(nextState StateName, errType *ErrorType) error { + return mec.continueFlow(nextState, errType) +} diff --git a/backend/flowpilot/context_methodInitialization.go b/backend/flowpilot/context_methodInitialization.go new file mode 100644 index 000000000..4da961fe6 --- /dev/null +++ b/backend/flowpilot/context_methodInitialization.go @@ -0,0 +1,25 @@ +package flowpilot + +import "hanko_flowsc/flowpilot/jsonmanager" + +// defaultMethodInitializationContext is the default implementation of the methodInitializationContext interface. +type defaultMethodInitializationContext struct { + schema Schema // Schema for method initialization. + isSuspended bool // Flag indicating if the method is suspended. + stash jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing stash data. +} + +// AddInputs adds input data to the Schema and returns a DefaultSchema instance. +func (mic *defaultMethodInitializationContext) AddInputs(inputList ...*DefaultInput) *DefaultSchema { + return mic.schema.AddInputs(inputList...) +} + +// SuspendMethod sets the isSuspended flag to indicate the method is suspended. +func (mic *defaultMethodInitializationContext) SuspendMethod() { + mic.isSuspended = true +} + +// Stash returns the ReadOnlyJSONManager for accessing stash data. +func (mic *defaultMethodInitializationContext) Stash() jsonmanager.ReadOnlyJSONManager { + return mic.stash +} diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go new file mode 100644 index 000000000..b5af87c7a --- /dev/null +++ b/backend/flowpilot/db.go @@ -0,0 +1,179 @@ +package flowpilot + +import ( + "fmt" + "github.com/gofrs/uuid" + "time" +) + +// FlowModel represents the database model for a Flow. +type FlowModel struct { + ID uuid.UUID // Unique ID of the Flow. + CurrentState StateName // Current state of the Flow. + PreviousState *StateName // Previous state of the Flow. + StashData string // Stash data associated with the Flow. + Version int // Version of the Flow. + Completed bool // Flag indicating if the Flow is completed. + ExpiresAt time.Time // Expiry time of the Flow. + CreatedAt time.Time // Creation time of the Flow. + UpdatedAt time.Time // Update time of the Flow. +} + +// TransitionModel represents the database model for a Transition. +type TransitionModel struct { + ID uuid.UUID // Unique ID of the Transition. + FlowID uuid.UUID // ID of the associated Flow. + Method MethodName // Name of the method associated with the Transition. + FromState StateName // Source state of the Transition. + ToState StateName // Target state of the Transition. + InputData string // Input data associated with the Transition. + ErrorCode *string // Optional error code associated with the Transition. + CreatedAt time.Time // Creation time of the Transition. + UpdatedAt time.Time // Update time of the Transition. +} + +// FlowDB is the interface for interacting with the Flow database. +type FlowDB interface { + GetFlow(flowID uuid.UUID) (*FlowModel, error) + CreateFlow(flowModel FlowModel) error + UpdateFlow(flowModel FlowModel) error + CreateTransition(transitionModel TransitionModel) error + + // TODO: "FindLastTransitionWithMethod" might be useless, or can be replaced. + + FindLastTransitionWithMethod(flowID uuid.UUID, method MethodName) (*TransitionModel, error) +} + +// FlowDBWrapper is an extended FlowDB interface that includes additional methods. +type FlowDBWrapper interface { + FlowDB + CreateFlowWithParam(p flowCreationParam) (*FlowModel, error) + UpdateFlowWithParam(p flowUpdateParam) (*FlowModel, error) + CreateTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) +} + +// DefaultFlowDBWrapper wraps a FlowDB instance to provide additional functionality. +type DefaultFlowDBWrapper struct { + FlowDB +} + +// wrapDB wraps a FlowDB instance to provide FlowDBWrapper functionality. +func wrapDB(db FlowDB) FlowDBWrapper { + return &DefaultFlowDBWrapper{FlowDB: db} +} + +// flowCreationParam holds parameters for creating a new Flow. +type flowCreationParam struct { + currentState StateName // Initial state of the Flow. + expiresAt time.Time // Expiry time of the Flow. +} + +// CreateFlowWithParam creates a new Flow with the given parameters. +func (w *DefaultFlowDBWrapper) CreateFlowWithParam(p flowCreationParam) (*FlowModel, error) { + // Generate a new UUID for the Flow. + flowID, err := uuid.NewV4() + if err != nil { + return nil, fmt.Errorf("failed to generate a new Flow id: %w", err) + } + + // Prepare the FlowModel for creation. + fm := FlowModel{ + ID: flowID, + CurrentState: p.currentState, + PreviousState: nil, + StashData: "{}", + Version: 0, + Completed: false, + ExpiresAt: p.expiresAt, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + // Create the Flow in the database. + err = w.CreateFlow(fm) + if err != nil { + return nil, fmt.Errorf("failed to store a new Flow to the dbw: %w", err) + } + + return &fm, nil +} + +// flowUpdateParam holds parameters for updating a Flow. +type flowUpdateParam struct { + flowID uuid.UUID // ID of the Flow to update. + nextState StateName // Next state of the Flow. + previousState *StateName // Previous state of the Flow. + stashData string // Updated stash data for the Flow. + version int // Updated version of the Flow. + completed bool // Flag indicating if the Flow is completed. + expiresAt time.Time // Updated expiry time of the Flow. + createdAt time.Time // Original creation time of the Flow. +} + +// UpdateFlowWithParam updates the specified Flow with the given parameters. +func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowModel, error) { + // Prepare the updated FlowModel. + fm := FlowModel{ + ID: p.flowID, + CurrentState: p.nextState, + PreviousState: p.previousState, + StashData: p.stashData, + Version: p.version, + Completed: p.completed, + ExpiresAt: p.expiresAt, + UpdatedAt: time.Now().UTC(), + CreatedAt: p.createdAt, + } + + // Update the Flow in the database. + err := w.UpdateFlow(fm) + if err != nil { + return nil, fmt.Errorf("failed to store updated Flow to the dbw: %w", err) + } + + return &fm, nil +} + +// transitionCreationParam holds parameters for creating a new Transition. +type transitionCreationParam struct { + flowID uuid.UUID // ID of the associated Flow. + methodName MethodName // Name of the method associated with the Transition. + fromState StateName // Source state of the Transition. + toState StateName // Target state of the Transition. + inputData string // Input data associated with the Transition. + errType *ErrorType // Optional error type associated with the Transition. +} + +// CreateTransitionWithParam creates a new Transition with the given parameters. +func (w *DefaultFlowDBWrapper) CreateTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) { + // Generate a new UUID for the Transition. + transitionID, err := uuid.NewV4() + if err != nil { + return nil, fmt.Errorf("failed to generate new Transition id: %w", err) + } + + // Prepare the TransitionModel for creation. + tm := TransitionModel{ + ID: transitionID, + FlowID: p.flowID, + Method: p.methodName, + FromState: p.fromState, + ToState: p.toState, + InputData: p.inputData, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + // Set the error code if provided. + if p.errType != nil { + tm.ErrorCode = &p.errType.Code + } + + // Create the Transition in the database. + err = w.CreateTransition(tm) + if err != nil { + return nil, fmt.Errorf("failed to store a new Transition to the dbw: %w", err) + } + + return &tm, err +} diff --git a/backend/flowpilot/errors.go b/backend/flowpilot/errors.go new file mode 100644 index 000000000..9b8bd8302 --- /dev/null +++ b/backend/flowpilot/errors.go @@ -0,0 +1,23 @@ +package flowpilot + +// TODO: Guess it would be nice to add an error interface + +// ErrorType represents a custom error type with a code and message. +type ErrorType struct { + Code string `json:"code"` // Unique error code. + Message string `json:"message"` // Description of the error. +} + +// Predefined error types +var ( + TechnicalError = &ErrorType{Code: "technical_error", Message: "Something went wrong."} + FlowExpiredError = &ErrorType{Code: "flow_expired_error", Message: "The flow has expired."} + FlowDiscontinuityError = &ErrorType{Code: "flow_discontinuity_error", Message: "The flow can't be continued."} + OperationNotPermittedError = &ErrorType{Code: "operation_not_permitted_error", Message: "The operation is not permitted."} + FormDataInvalidError = &ErrorType{Code: "form_data_invalid_error", Message: "Form data invalid."} + EmailInvalidError = &ErrorType{Code: "email_invalid_error", Message: "The email address is invalid."} + ValueMissingError = &ErrorType{Code: "value_missing_error", Message: "Missing value."} + ValueInvalidError = &ErrorType{Code: "value_invalid_error", Message: "The value is invalid."} + ValueTooLongError = &ErrorType{Code: "value_too_long_error", Message: "Value is too long."} + ValueTooShortError = &ErrorType{Code: "value_too_short_error", Message: "Value is too short."} +) diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go new file mode 100644 index 000000000..1e929d71d --- /dev/null +++ b/backend/flowpilot/flow.go @@ -0,0 +1,175 @@ +package flowpilot + +import ( + "errors" + "fmt" + "time" +) + +// InputData holds input data in JSON format. +type InputData struct { + JSONString string `json:"input_data"` +} + +// getJSONStringOrDefault returns the JSON string or a default "{}" value. +func (i InputData) getJSONStringOrDefault() string { + if len(i.JSONString) == 0 { + return "{}" + } + + return i.JSONString +} + +// flowExecutionOptions represents options for executing a Flow. +type flowExecutionOptions struct { + action string + inputData InputData +} + +// WithActionParam sets the MethodName for flowExecutionOptions. +func WithActionParam(action string) func(*flowExecutionOptions) { + return func(f *flowExecutionOptions) { + f.action = action + } +} + +// WithInputData sets the InputData for flowExecutionOptions. +func WithInputData(inputData InputData) func(*flowExecutionOptions) { + return func(f *flowExecutionOptions) { + f.inputData = inputData + } +} + +// TODO: Not sure if we really want to have these types for state and method names + +// StateName represents the name of a state in a Flow. +type StateName string + +// MethodName represents the name of a method associated with a Transition. +type MethodName string + +// TODO: Should it be possible to partially implement the Method interface? E.g. when a method does not require initialization. + +// Method defines the interface for flow methods. +type Method interface { + GetName() MethodName // Get the method name. + GetDescription() string // Get the method description. + Initialize(InitializationContext) // Initialize the method. + Execute(ExecutionContext) error // Execute the method. +} + +// Transition holds a method associated with a state transition. +type Transition struct { + Method Method +} + +// Transitions is a collection of Transition instances. +type Transitions []Transition + +// getMethod returns the Method associated with the specified name. +func (ts *Transitions) getMethod(methodName MethodName) (Method, error) { + for _, t := range *ts { + if t.Method.GetName() == methodName { + return t.Method, nil + } + } + + return nil, errors.New(fmt.Sprintf("method '%s' not valid", methodName)) +} + +// StateTransitions maps states to associated Transitions. +type StateTransitions map[StateName]Transitions + +// Flow defines a flow structure with states, transitions, and settings. +type Flow struct { + Flow StateTransitions // State transitions mapping. + Path string // Flow path or identifier. + InitialState StateName // Initial state of the flow. + ErrorState StateName // State representing errors. + EndState StateName // Final state of the flow. + TTL time.Duration // Time-to-live for the flow. +} + +// stateExists checks if a state exists in the Flow. +func (f *Flow) stateExists(stateName StateName) bool { + _, ok := f.Flow[stateName] + return ok +} + +// getTransitionsForState returns transitions for a specified state. +func (f *Flow) getTransitionsForState(stateName StateName) *Transitions { + if ts, ok := f.Flow[stateName]; ok && len(ts) > 0 { + return &ts + } + return nil +} + +// setDefaults sets default values for Flow settings. +func (f *Flow) setDefaults() { + if f.TTL.Seconds() == 0 { + f.TTL = time.Minute * 60 + } +} + +// validate performs validation checks on the Flow configuration. +func (f *Flow) validate() error { + // Validate fixed states and their presence in the Flow. + if len(f.InitialState) == 0 { + return errors.New("fixed state 'InitialState' is not set") + } + if len(f.ErrorState) == 0 { + return errors.New("fixed state 'ErrorState' is not set") + } + if len(f.EndState) == 0 { + return errors.New("fixed state 'EndState' is not set") + } + if !f.stateExists(f.InitialState) { + return errors.New("fixed state 'InitialState' does not belong to the flow") + } + if !f.stateExists(f.ErrorState) { + return errors.New("fixed state 'ErrorState' does not belong to the flow") + } + if !f.stateExists(f.EndState) { + return errors.New("fixed state 'EndState' does not belong to the flow") + } + if ts := f.getTransitionsForState(f.EndState); ts != nil { + return fmt.Errorf("the specified EndState '%s' is not allowed to have transitions", f.EndState) + } + + // TODO: Additional validation for unique State and Method names,... + + return nil +} + +// Execute handles the execution of actions for a Flow. +func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (*Response, error) { + // Process execution options. + var executionOptions flowExecutionOptions + for _, option := range opts { + option(&executionOptions) + } + + // Set default values for Flow settings. + f.setDefaults() + + // Perform validation checks on the Flow configuration. + if err := f.validate(); err != nil { + return nil, fmt.Errorf("invalid flow: %w", err) + } + + if len(executionOptions.action) == 0 { + // If the action is empty, create a new Flow. + return createAndInitializeFlow(db, *f) + } + + // Otherwise, update an existing Flow. + return executeFlowMethod(db, *f, executionOptions) +} + +// ErrorResponse returns an error response for the Flow. +func (f *Flow) ErrorResponse() *Response { + return &Response{ + State: f.ErrorState, + Error: TechnicalError, + } +} diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go new file mode 100644 index 000000000..d2ac1cf47 --- /dev/null +++ b/backend/flowpilot/input.go @@ -0,0 +1,228 @@ +package flowpilot + +import "regexp" + +// InputType represents the type of the input field. +type InputType string + +// Input types enumeration. +const ( + StringType InputType = "string" + EmailType InputType = "email" + NumberType InputType = "number" + PasswordType InputType = "password" + JSONType InputType = "json" +) + +// Input defines the interface for input fields. +type Input interface { + MinLength(minLength int) *DefaultInput + MaxLength(maxLength int) *DefaultInput + Required(b bool) *DefaultInput + Hidden(b bool) *DefaultInput + Preserve(b bool) *DefaultInput + Persist(b bool) *DefaultInput + + // TODO: After experimenting with 'ConditionalIncludeFromStash', I realized that it should be replaced with another + // function 'ConditionalIncludeOnStates(...StateName)'. This function would include the input field when executed + // under specific states. The issue with 'ConditionalIncludeFromStash' is, that if a method "forgets" to set the + // value beforehand, the validation won't require the value, since it's not present in the stash. In contrast, using + // 'ConditionalIncludeOnStates(...)' makes it clear that the value must be present when the current state matches + // and the decision about whether to validate the value or not isn't dependent on a different method. + + ConditionalIncludeFromStash(b bool) *DefaultInput + CompareWithStash(b bool) *DefaultInput + setValue(value interface{}) *DefaultInput + validate(value *string) bool +} + +// defaultExtraInputOptions holds additional input field options. +type defaultExtraInputOptions struct { + preserveValue bool + persistValue bool + conditionalIncludeFromStash bool + compareWithStash bool +} + +// DefaultInput represents an input field with its options. +type DefaultInput struct { + name string + dataType InputType + value interface{} + minLength *int + maxLength *int + required *bool + hidden *bool + errorType *ErrorType + defaultExtraInputOptions +} + +// PublicInput represents an input field for public exposure. +type PublicInput struct { + Name string `json:"name"` + Type InputType `json:"type"` + Value interface{} `json:"value,omitempty"` + MinLength *int `json:"min_length,omitempty"` + MaxLength *int `json:"max_length,omitempty"` + Required *bool `json:"required,omitempty"` + Hidden *bool `json:"hidden,omitempty"` + Error *ErrorType `json:"error,omitempty"` +} + +// newInput creates a new DefaultInput instance with provided parameters. +func newInput(name string, t InputType, persistValue bool) *DefaultInput { + return &DefaultInput{ + name: name, + dataType: t, + defaultExtraInputOptions: defaultExtraInputOptions{ + preserveValue: false, + persistValue: persistValue, + conditionalIncludeFromStash: false, + compareWithStash: false, + }, + } +} + +// StringInput creates a new input field of string type. +func StringInput(name string) *DefaultInput { + return newInput(name, StringType, true) +} + +// EmailInput creates a new input field of email type. +func EmailInput(name string) *DefaultInput { + return newInput(name, EmailType, true) +} + +// NumberInput creates a new input field of number type. +func NumberInput(name string) *DefaultInput { + return newInput(name, NumberType, true) +} + +// PasswordInput creates a new input field of password type. +func PasswordInput(name string) *DefaultInput { + return newInput(name, PasswordType, false) +} + +// JSONInput creates a new input field of JSON type. +func JSONInput(name string) *DefaultInput { + return newInput(name, JSONType, false) +} + +// MinLength sets the minimum length for the input field. +func (i *DefaultInput) MinLength(minLength int) *DefaultInput { + i.minLength = &minLength + return i +} + +// MaxLength sets the maximum length for the input field. +func (i *DefaultInput) MaxLength(maxLength int) *DefaultInput { + i.maxLength = &maxLength + return i +} + +// Required sets whether the input field is required. +func (i *DefaultInput) Required(b bool) *DefaultInput { + i.required = &b + return i +} + +// Hidden sets whether the input field is hidden. +func (i *DefaultInput) Hidden(b bool) *DefaultInput { + i.hidden = &b + return i +} + +// Preserve sets whether the input field value should be preserved, so that the value is included in the response +// instead of being blanked out. +func (i *DefaultInput) Preserve(b bool) *DefaultInput { + i.preserveValue = b + return i +} + +// Persist sets whether the input field value should be persisted. +func (i *DefaultInput) Persist(b bool) *DefaultInput { + i.persistValue = b + return i +} + +// ConditionalIncludeFromStash sets whether the input field is conditionally included from the stash. +func (i *DefaultInput) ConditionalIncludeFromStash(b bool) *DefaultInput { + i.conditionalIncludeFromStash = b + return i +} + +// CompareWithStash sets whether the input field is compared with stash values. +func (i *DefaultInput) CompareWithStash(b bool) *DefaultInput { + i.compareWithStash = b + return i +} + +// setValue sets the value for the input field. +func (i *DefaultInput) setValue(value interface{}) *DefaultInput { + i.value = &value + return i +} + +// validate performs validation on the input field. +func (i *DefaultInput) validate(inputValue *string, stashValue *string) bool { + // Validate based on input field options. + + // TODO: Replace with more structured validation logic. + + if i.conditionalIncludeFromStash && stashValue == nil { + return true + } + + if i.required != nil && *i.required && (inputValue == nil || len(*inputValue) <= 0) { + i.errorType = ValueMissingError + return false + } + + if i.compareWithStash && inputValue != nil && stashValue != nil && *inputValue != *stashValue { + i.errorType = ValueInvalidError + return false + } + + if i.dataType == JSONType { + // skip further validation + return true + } + + if i.minLength != nil { + if len(*inputValue) < *i.minLength { + i.errorType = ValueTooShortError + return false + } + } + + if i.maxLength != nil { + if len(*inputValue) > *i.maxLength { + i.errorType = ValueTooLongError + return false + } + } + + if i.dataType == EmailType { + pattern := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + if matched := pattern.MatchString(*inputValue); !matched { + i.errorType = EmailInvalidError + return false + } + } + + return true +} + +// toPublicInput converts the DefaultInput to a PublicInput for public exposure. +func (i *DefaultInput) toPublicInput() *PublicInput { + return &PublicInput{ + Name: i.name, + Type: i.dataType, + Value: i.value, + MinLength: i.minLength, + MaxLength: i.maxLength, + Required: i.required, + Hidden: i.hidden, + Error: i.errorType, + } +} diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go new file mode 100644 index 000000000..74decd18a --- /dev/null +++ b/backend/flowpilot/input_schema.go @@ -0,0 +1,157 @@ +package flowpilot + +import ( + "fmt" + "hanko_flowsc/flowpilot/jsonmanager" +) + +// Schema represents an interface for managing input data schemas. +type Schema interface { + AddInputs(inputList ...*DefaultInput) *DefaultSchema +} + +// MethodExecutionSchema represents an interface for managing method execution schemas. +type MethodExecutionSchema interface { + SetError(inputName string, errType *ErrorType) + toResponseSchema() ResponseSchema +} + +// ResponseSchema represents an interface for response schemas. +type ResponseSchema interface { + GetInput(name string) *DefaultInput + preserveInputData(inputData jsonmanager.ReadOnlyJSONManager) + getTransitionData(inputData jsonmanager.ReadOnlyJSONManager) (jsonmanager.ReadOnlyJSONManager, error) + applyFlash(flashData jsonmanager.ReadOnlyJSONManager) + applyStash(stashData jsonmanager.ReadOnlyJSONManager) + validateInputData(inputData jsonmanager.ReadOnlyJSONManager, stash jsonmanager.ReadOnlyJSONManager) bool + toPublicInputs() PublicInputs +} + +// Inputs represents a collection of DefaultInput instances. +type Inputs []*DefaultInput + +// PublicInputs represents a collection of PublicInput instances. +type PublicInputs []*PublicInput + +// DefaultSchema implements the Schema interface and holds a collection of input fields. +type DefaultSchema struct { + Inputs +} + +// toResponseSchema converts the DefaultSchema to a ResponseSchema. +func (s *DefaultSchema) toResponseSchema() ResponseSchema { + return s +} + +// AddInputs adds input fields to the DefaultSchema and returns the updated schema. +func (s *DefaultSchema) AddInputs(inputList ...*DefaultInput) *DefaultSchema { + for _, i := range inputList { + s.Inputs = append(s.Inputs, i) + } + + return s +} + +// GetInput retrieves an input field from the schema based on its name. +func (s *DefaultSchema) GetInput(name string) *DefaultInput { + for _, i := range s.Inputs { + if i.name == name { + return i + } + } + + return nil +} + +// SetError sets an error type for an input field in the schema. +func (s *DefaultSchema) SetError(inputName string, errType *ErrorType) { + if i := s.GetInput(inputName); i != nil { + i.errorType = errType + } +} + +// validateInputData validates the input data based on the input definitions in the schema. +func (s *DefaultSchema) validateInputData(inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.ReadOnlyJSONManager) bool { + valid := true + + for _, i := range s.Inputs { + var inputValue *string + var stashValue *string + + if v := inputData.Get(i.name); v.Exists() { + inputValue = &v.Str + } + + if v := stashData.Get(i.name); v.Exists() { + stashValue = &v.Str + } + + if !i.validate(inputValue, stashValue) && valid { + valid = false + } + } + + return valid +} + +// preserveInputData preserves input data by setting values of inputs that should be preserved. +func (s *DefaultSchema) preserveInputData(inputData jsonmanager.ReadOnlyJSONManager) { + for _, i := range s.Inputs { + if v := inputData.Get(i.name); v.Exists() { + if i.preserveValue { + i.setValue(v.Str) + } + } + } +} + +// getTransitionData filters input data to persist based on schema definitions. +func (s *DefaultSchema) getTransitionData(inputData jsonmanager.ReadOnlyJSONManager) (jsonmanager.ReadOnlyJSONManager, error) { + toPersist := jsonmanager.NewJSONManager() + + for _, i := range s.Inputs { + if v := inputData.Get(i.name); v.Exists() && i.persistValue { + if err := toPersist.Set(i.name, v.Value()); err != nil { + return nil, fmt.Errorf("failed to copy data: %v", err) + } + } + } + + return toPersist, nil +} + +// applyFlash updates input values in the schema with corresponding values from flash data. +func (s *DefaultSchema) applyFlash(flashData jsonmanager.ReadOnlyJSONManager) { + for _, i := range s.Inputs { + v := flashData.Get(i.name) + + if v.Exists() { + i.setValue(v.Value()) + } + } +} + +// applyStash updates input values in the schema with corresponding values from stash data. +func (s *DefaultSchema) applyStash(stashData jsonmanager.ReadOnlyJSONManager) { + n := 0 + + for _, i := range s.Inputs { + if !i.conditionalIncludeFromStash || stashData.Get(i.name).Exists() { + s.Inputs[n] = i + n++ + } + } + + s.Inputs = s.Inputs[:n] +} + +// toPublicInputs converts DefaultSchema to PublicInputs for public exposure. +func (s *DefaultSchema) toPublicInputs() PublicInputs { + var pi PublicInputs + + for _, i := range s.Inputs { + pi = append(pi, i.toPublicInput()) + } + + return pi +} diff --git a/backend/flowpilot/jsonmanager/manager.go b/backend/flowpilot/jsonmanager/manager.go new file mode 100644 index 000000000..de88cb203 --- /dev/null +++ b/backend/flowpilot/jsonmanager/manager.go @@ -0,0 +1,84 @@ +package jsonmanager + +import ( + "errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// ReadJSONManager is the interface that allows read operations. +type ReadJSONManager interface { + Get(path string) gjson.Result // Get retrieves the value at the specified path in the JSON data. + String() string // String returns the JSON data as a string. + Unmarshal() interface{} // Unmarshal parses the JSON data and returns it as an interface{}. +} + +// JSONManager is the interface that defines methods for reading, writing, and deleting JSON data. +type JSONManager interface { + ReadJSONManager + Set(path string, value interface{}) error // Set updates the JSON data at the specified path with the provided value. + Delete(path string) error // Delete removes a value from the JSON data at the specified path. +} + +// ReadOnlyJSONManager is the interface that allows only read operations. +type ReadOnlyJSONManager interface { + ReadJSONManager +} + +// DefaultJSONManager is the default implementation of the JSONManager interface. +type DefaultJSONManager struct { + data string // The JSON data stored as a string. +} + +// NewJSONManager creates a new instance of DefaultJSONManager with empty JSON data. +func NewJSONManager() JSONManager { + return &DefaultJSONManager{data: "{}"} +} + +// NewJSONManagerFromString creates a new instance of DefaultJSONManager with the given JSON data. +// It checks if the provided data is valid JSON before creating the instance. +func NewJSONManagerFromString(data string) (JSONManager, error) { + if !gjson.Valid(data) { + return nil, errors.New("invalid json") + } + return &DefaultJSONManager{data: data}, nil +} + +// Get retrieves the value at the specified path in the JSON data. +func (jm *DefaultJSONManager) Get(path string) gjson.Result { + return gjson.Get(jm.data, path) +} + +// Set updates the JSON data at the specified path with the provided value. +func (jm *DefaultJSONManager) Set(path string, value interface{}) error { + newData, err := sjson.Set(jm.data, path, value) + if err != nil { + return err + } + jm.data = newData + return nil +} + +// Delete removes a value from the JSON data at the specified path. +func (jm *DefaultJSONManager) Delete(path string) error { + newData, err := sjson.Delete(jm.data, path) + if err != nil { + return err + } + jm.data = newData + return nil +} + +// String returns the JSON data as a string. +func (jm *DefaultJSONManager) String() string { + return jm.data +} + +// Unmarshal parses the JSON data and returns it as an interface{}. +func (jm *DefaultJSONManager) Unmarshal() interface{} { + m, ok := gjson.Parse(jm.data).Value().(interface{}) + if !ok { + return nil + } + return m +} diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go new file mode 100644 index 000000000..c2d6f29de --- /dev/null +++ b/backend/flowpilot/response.go @@ -0,0 +1,119 @@ +package flowpilot + +import ( + "fmt" + "hanko_flowsc/flowpilot/jsonmanager" + "hanko_flowsc/flowpilot/utils" +) + +// Link represents a link to an action. +type Link struct { + Href string `json:"href"` + Inputs PublicInputs `json:"inputs"` + MethodName MethodName `json:"method_name"` + Description string `json:"description"` +} + +// Links is a collection of Link instances. +type Links []Link + +// Add adds a link to the collection of Links. +func (ls *Links) Add(l Link) { + *ls = append(*ls, l) +} + +// Response represents the response of an action execution. +type Response struct { + State StateName `json:"state"` + Payload interface{} `json:"payload,omitempty"` + Links Links `json:"links"` + Error *ErrorType `json:"error,omitempty"` +} + +// methodExecutionResult holds the result of a method execution. +type methodExecutionResult struct { + methodName MethodName + schema ResponseSchema + inputData jsonmanager.ReadOnlyJSONManager +} + +// executionResult holds the result of an action execution. +type executionResult struct { + nextState StateName + errType *ErrorType + + *methodExecutionResult +} + +// generateResponse generates a response based on the execution result. +func (er *executionResult) generateResponse(fc defaultFlowContext) (*Response, error) { + var links Links + + transitions := fc.flow.getTransitionsForState(er.nextState) + + if transitions != nil { + for _, t := range *transitions { + currentMethodName := t.Method.GetName() + currentDescription := t.Method.GetDescription() + + href := er.createHref(fc, currentMethodName) + + var schema ResponseSchema + + if schema = er.getExecutionSchema(currentMethodName); schema == nil { + defaultSchema := DefaultSchema{} + mic := defaultMethodInitializationContext{schema: &defaultSchema, stash: fc.stash} + + t.Method.Initialize(&mic) + + if mic.isSuspended { + continue + } + + schema = &defaultSchema + } + + schema.applyFlash(fc.flash) + schema.applyStash(fc.stash) + + link := Link{ + Href: href, + Inputs: schema.toPublicInputs(), + MethodName: currentMethodName, + Description: currentDescription, + } + + links.Add(link) + } + } + + resp := &Response{ + State: er.nextState, + Payload: fc.payload.Unmarshal(), + Links: links, + Error: er.errType, + } + + return resp, nil +} + +// getExecutionSchema gets the execution schema for a given method name. +func (er *executionResult) getExecutionSchema(methodName MethodName) ResponseSchema { + if er.methodExecutionResult == nil || methodName != er.methodExecutionResult.methodName { + // The current method result does not belong to the methodName. + return nil + } + + schema := er.methodExecutionResult.schema + inputData := er.methodExecutionResult.inputData + + schema.preserveInputData(inputData) + + return schema +} + +// createHref creates a link HREF based on the current flow context and method name. +func (er *executionResult) createHref(fc defaultFlowContext, methodName MethodName) string { + action := utils.CreateActionParam(string(methodName), fc.GetFlowID()) + return fmt.Sprintf("%s?flowpilot_action=%s", fc.GetPath(), action) +} diff --git a/backend/flowpilot/utils/param.go b/backend/flowpilot/utils/param.go new file mode 100644 index 000000000..465f8103d --- /dev/null +++ b/backend/flowpilot/utils/param.go @@ -0,0 +1,46 @@ +package utils + +import ( + "fmt" + "github.com/gofrs/uuid" + "strings" +) + +// ParsedAction represents a parsed action from an input string. +type ParsedAction struct { + MethodName string // The name of the method extracted from the input string. + FlowID uuid.UUID // The UUID representing the flow ID extracted from the input string. +} + +// ParseActionParam parses an input string to extract method name and flow ID. +func ParseActionParam(inputString string) (*ParsedAction, error) { + if inputString == "" { + return nil, fmt.Errorf("input string is empty") + } + + // Split the input string into method and flow ID parts using "@" as separator. + parts := strings.SplitN(inputString, "@", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid input string format") + } + + // Extract method name from the first part of the split. + method := parts[0] + if len(method) == 0 { + return nil, fmt.Errorf("first part of input string is empty") + } + + // Parse the second part of the input string into a UUID representing the flow ID. + flowID, err := uuid.FromString(parts[1]) + if err != nil { + return nil, fmt.Errorf("failed to parse second part of the input string: %w", err) + } + + // Return a ParsedAction instance with extracted method name and flow ID. + return &ParsedAction{MethodName: method, FlowID: flowID}, nil +} + +// CreateActionParam creates an input string from method name and flow ID. +func CreateActionParam(method string, flowID uuid.UUID) string { + return fmt.Sprintf("%s@%s", method, flowID) +} From 09bf6131c2f24e8afeb6810b1953a2984f5aa7f8 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 10 Aug 2023 13:41:22 +0200 Subject: [PATCH 002/278] chore: install dependencies --- backend/flowpilot/context.go | 9 ++++++--- backend/flowpilot/context_flow.go | 2 +- backend/flowpilot/context_methodExecution.go | 2 +- backend/flowpilot/context_methodInitialization.go | 2 +- backend/flowpilot/errors.go | 1 - backend/flowpilot/input_schema.go | 2 +- backend/flowpilot/response.go | 4 ++-- backend/go.mod | 4 ++++ backend/go.sum | 9 +++++++++ 9 files changed, 25 insertions(+), 10 deletions(-) diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 83b1adeae..68cc9e95b 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "github.com/gofrs/uuid" - "hanko_flowsc/flowpilot/jsonmanager" - "hanko_flowsc/flowpilot/utils" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" "time" ) @@ -20,6 +20,9 @@ type flowContext interface { Payload() jsonmanager.JSONManager // Stash returns the JSONManager for accessing stash data. Stash() jsonmanager.JSONManager + + // TODO: Maybe we should remove 'Flash' and provide the same functionality under "Input()" + // Flash returns the JSONManager for accessing flash data. Flash() jsonmanager.JSONManager // GetInitialState returns the initial state of the Flow. @@ -60,7 +63,7 @@ type methodExecutionContext interface { // ValidateInputData validates the input data against the schema. ValidateInputData() bool - // TODO: CopyInputsToStash can maybe removed or replaced with an option you can set via the input options. + // TODO: CopyInputsToStash can maybe removed or replaced with an option you can set via the input schema. // CopyInputsToStash copies specified inputs to the stash. CopyInputsToStash(inputNames ...string) error diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index 7c0429a3b..f10328d71 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -3,7 +3,7 @@ package flowpilot import ( "fmt" "github.com/gofrs/uuid" - "hanko_flowsc/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" ) // defaultFlowContext is the default implementation of the flowContext interface. diff --git a/backend/flowpilot/context_methodExecution.go b/backend/flowpilot/context_methodExecution.go index 5ab38376d..67bf49290 100644 --- a/backend/flowpilot/context_methodExecution.go +++ b/backend/flowpilot/context_methodExecution.go @@ -3,7 +3,7 @@ package flowpilot import ( "errors" "fmt" - "hanko_flowsc/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" ) // defaultMethodExecutionContext is the default implementation of the methodExecutionContext interface. diff --git a/backend/flowpilot/context_methodInitialization.go b/backend/flowpilot/context_methodInitialization.go index 4da961fe6..95ce591da 100644 --- a/backend/flowpilot/context_methodInitialization.go +++ b/backend/flowpilot/context_methodInitialization.go @@ -1,6 +1,6 @@ package flowpilot -import "hanko_flowsc/flowpilot/jsonmanager" +import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" // defaultMethodInitializationContext is the default implementation of the methodInitializationContext interface. type defaultMethodInitializationContext struct { diff --git a/backend/flowpilot/errors.go b/backend/flowpilot/errors.go index 9b8bd8302..5359c1f1e 100644 --- a/backend/flowpilot/errors.go +++ b/backend/flowpilot/errors.go @@ -12,7 +12,6 @@ type ErrorType struct { var ( TechnicalError = &ErrorType{Code: "technical_error", Message: "Something went wrong."} FlowExpiredError = &ErrorType{Code: "flow_expired_error", Message: "The flow has expired."} - FlowDiscontinuityError = &ErrorType{Code: "flow_discontinuity_error", Message: "The flow can't be continued."} OperationNotPermittedError = &ErrorType{Code: "operation_not_permitted_error", Message: "The operation is not permitted."} FormDataInvalidError = &ErrorType{Code: "form_data_invalid_error", Message: "Form data invalid."} EmailInvalidError = &ErrorType{Code: "email_invalid_error", Message: "The email address is invalid."} diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index 74decd18a..33efc7014 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -2,7 +2,7 @@ package flowpilot import ( "fmt" - "hanko_flowsc/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" ) // Schema represents an interface for managing input data schemas. diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index c2d6f29de..e89da8b4b 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -2,8 +2,8 @@ package flowpilot import ( "fmt" - "hanko_flowsc/flowpilot/jsonmanager" - "hanko_flowsc/flowpilot/utils" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // Link represents a link to an action. diff --git a/backend/go.mod b/backend/go.mod index 0e9d9a499..1ab92fe06 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -39,6 +39,8 @@ require ( github.com/sethvargo/go-redisstore v0.3.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 + github.com/tidwall/gjson v1.16.0 + github.com/tidwall/sjson v1.2.5 golang.org/x/crypto v0.24.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/oauth2 v0.21.0 @@ -144,6 +146,8 @@ require ( github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect diff --git a/backend/go.sum b/backend/go.sum index 0db74803b..861c3e1fa 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -592,7 +592,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= +github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= From 443a920a516d97bf2cbf51e4fef551ead106ee0e Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 10 Aug 2023 13:45:05 +0200 Subject: [PATCH 003/278] chore: add migration --- .../20230810173315_create_flows.down.fizz | 2 ++ .../20230810173315_create_flows.up.fizz | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 backend/persistence/migrations/20230810173315_create_flows.down.fizz create mode 100644 backend/persistence/migrations/20230810173315_create_flows.up.fizz diff --git a/backend/persistence/migrations/20230810173315_create_flows.down.fizz b/backend/persistence/migrations/20230810173315_create_flows.down.fizz new file mode 100644 index 000000000..a109d8c16 --- /dev/null +++ b/backend/persistence/migrations/20230810173315_create_flows.down.fizz @@ -0,0 +1,2 @@ +drop_table("transitions") +drop_table("flows") diff --git a/backend/persistence/migrations/20230810173315_create_flows.up.fizz b/backend/persistence/migrations/20230810173315_create_flows.up.fizz new file mode 100644 index 000000000..56a9a7a10 --- /dev/null +++ b/backend/persistence/migrations/20230810173315_create_flows.up.fizz @@ -0,0 +1,22 @@ +create_table("flows") { + t.Column("id", "uuid", {primary: true}) + t.Column("current_state", "string") + t.Column("previous_state", "string", {"null": true}) + t.Column("stash_data", "string") + t.Column("version", "int") + t.Column("completed", "bool", {"default": false}) + t.Column("expires_at", "timestamp") + t.Timestamps() +} + +create_table("transitions") { + t.Column("id", "uuid", {primary: true}) + t.Column("flow_id", "uuid") + t.Column("method", "string") + t.Column("from_state", "string") + t.Column("to_state", "string") + t.Column("input_data", "string") + t.Column("error_code", "string", {"null": true}) + t.ForeignKey("flow_id", {"flows": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.Timestamps() +} \ No newline at end of file From 82950f831af72e7288758eb7db88efb781ec4fe3 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 10 Aug 2023 16:51:07 +0200 Subject: [PATCH 004/278] chore: example integration --- backend/flow_api_test/config.go | 35 ++ backend/flow_api_test/echo.go | 49 +++ backend/flow_api_test/flow.go | 24 ++ backend/flow_api_test/helper.go | 50 +++ backend/flow_api_test/methods.go | 319 ++++++++++++++++++ .../flow_api_test/static/generic_client.html | 270 +++++++++++++++ backend/flow_api_test/types.go | 34 ++ backend/flowpilot/errors.go | 1 + backend/handler/public_router.go | 1 + backend/persistence/models/flow.go | 65 ++++ backend/persistence/models/flowdb.go | 179 ++++++++++ backend/persistence/models/transition.go | 58 ++++ 12 files changed, 1085 insertions(+) create mode 100644 backend/flow_api_test/config.go create mode 100644 backend/flow_api_test/echo.go create mode 100644 backend/flow_api_test/flow.go create mode 100644 backend/flow_api_test/helper.go create mode 100644 backend/flow_api_test/methods.go create mode 100644 backend/flow_api_test/static/generic_client.html create mode 100644 backend/flow_api_test/types.go create mode 100644 backend/persistence/models/flow.go create mode 100644 backend/persistence/models/flowdb.go create mode 100644 backend/persistence/models/transition.go diff --git a/backend/flow_api_test/config.go b/backend/flow_api_test/config.go new file mode 100644 index 000000000..423c57440 --- /dev/null +++ b/backend/flow_api_test/config.go @@ -0,0 +1,35 @@ +package flow_api_test + +const ( + FlowOptionPasscodeOnly uint8 = 1 << iota // passcodes enabled + FlowOptionSecondFactorFlow // use second factor flow (email verification and passwords must be enabled) + FlowOptionEmailVerification // enable email verification + FlowOptionPasswords // enable passwords +) + +type FlowConfig struct { + FlowOption uint8 `json:"flow_option"` +} + +func (c *FlowConfig) isEnabled(option uint8) bool { + return c.FlowOption&option != 0 +} +func (c *FlowConfig) IsValid() bool { + validConfigurations := []uint8{ + FlowOptionPasscodeOnly, + FlowOptionEmailVerification, + FlowOptionPasswords, + FlowOptionEmailVerification | FlowOptionPasswords, + FlowOptionSecondFactorFlow | FlowOptionEmailVerification | FlowOptionPasswords, + } + + for _, validOption := range validConfigurations { + if c.FlowOption == validOption { + return true + } + } + + return false +} + +var myFlowConfig FlowConfig diff --git a/backend/flow_api_test/echo.go b/backend/flow_api_test/echo.go new file mode 100644 index 000000000..8c00ecfe9 --- /dev/null +++ b/backend/flow_api_test/echo.go @@ -0,0 +1,49 @@ +package flow_api_test + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" + "net/http" +) + +type FlowPilotHandler struct { + Persister persistence.Persister +} + +type FlowRequest struct { + flowpilot.InputData + FlowConfig +} + +func (e *FlowPilotHandler) LoginFlowHandler(c echo.Context) error { + actionParam := c.QueryParam("flowpilot_action") + + var body FlowRequest + _ = c.Bind(&body) + + flowConfig := FlowConfig{FlowOption: body.FlowOption} + if !flowConfig.IsValid() { + return fmt.Errorf("invalid flow option: %v", flowConfig) + } + + myFlowConfig = flowConfig + + return e.Persister.Transaction(func(tx *pop.Connection) error { + db := models.NewFlowDB(tx) + + flowResponse, err := Flow.Execute(db, + flowpilot.WithActionParam(actionParam), + flowpilot.WithInputData(body.InputData)) + + if err != nil { + c.Logger().Errorf("flowpilot error: %w", err) + return c.JSON(http.StatusOK, Flow.ErrorResponse()) + } + + return c.JSON(http.StatusOK, flowResponse) + }) +} diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go new file mode 100644 index 000000000..e62e9a78b --- /dev/null +++ b/backend/flow_api_test/flow.go @@ -0,0 +1,24 @@ +package flow_api_test + +import ( + "github.com/teamhanko/hanko/backend/flowpilot" + "time" +) + +var Flow = flowpilot.NewFlow("/flow_api_login"). + State(StateSignInOrSignUp, SubmitEmail{}, GetWAChallenge{}). + State(StateLoginWithPasskey, VerifyWAPublicKey{}, Back{}). + State(StateLoginWithPasscode, SubmitPasscodeCode{}, Back{}). + State(StateLoginWithPassword, SubmitExistingPassword{}, RequestRecovery{}, Back{}). + State(StateRecoverPasswordViaPasscode, SubmitPasscodeCode{}, Back{}). + State(StateUpdateExistingPassword, SubmitNewPassword{}). + State(StateConfirmAccountCreation, CreateUser{}, Back{}). + State(StatePasswordCreation, SubmitNewPassword{}). + State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipOnboarding{}). + State(StateCreatePasskey, VerifyWAAssertion{}). + State(StateVerifyEmailViaPasscode, SubmitPasscodeCode{}). + State(StateError). + State(StateSuccess). + FixedStates(StateSignInOrSignUp, StateError, StateSuccess). + TTL(time.Minute * 10). + Build() diff --git a/backend/flow_api_test/helper.go b/backend/flow_api_test/helper.go new file mode 100644 index 000000000..f9a0ee2d9 --- /dev/null +++ b/backend/flow_api_test/helper.go @@ -0,0 +1,50 @@ +package flow_api_test + +import ( + "crypto/rand" + "encoding/base64" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/flowpilot" + "strings" +) + +func initPasskey(c flowpilot.ExecutionContext) { + passkeyChallenge := "42" + _ = c.Stash().Set("passkey_public_key", passkeyChallenge) + _ = c.Payload().Set("challenge", passkeyChallenge) +} + +func initPasscode(c flowpilot.ExecutionContext, email string, generate2faToken bool) { + passcodeIDStash := c.Stash().Get("passcode_id") + emailStash := c.Stash().Get("email") + + if len(passcodeIDStash.String()) == 0 || emailStash.String() != email { + // resend passcode + id, _ := uuid.NewV4() + _ = c.Stash().Set("passcode_id", id.String()) + _ = c.Flash().Set("passcode_id", id.String()) + } else { + _ = c.Flash().Set("passcode_id", passcodeIDStash.String()) + } + + _ = c.Stash().Set("email", email) + _ = c.Stash().Set("code", "424242") + + if generate2faToken { + token, _ := generateToken(32) + _ = c.Stash().Set("passcode_2fa_token", token) + _ = c.Flash().Set("passcode_2fa_token", token) + } +} + +func generateToken(length int) (string, error) { + randomBytes := make([]byte, length) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + randomString := base64.URLEncoding.EncodeToString(randomBytes) + randomString = strings.ReplaceAll(randomString, "-", "") + randomString = strings.ReplaceAll(randomString, "_", "") + return randomString[:length], nil +} diff --git a/backend/flow_api_test/methods.go b/backend/flow_api_test/methods.go new file mode 100644 index 000000000..7698e0322 --- /dev/null +++ b/backend/flow_api_test/methods.go @@ -0,0 +1,319 @@ +package flow_api_test + +import ( + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence/models" +) + +type SubmitEmail struct{} + +func (m SubmitEmail) GetName() flowpilot.MethodName { + return MethodSubmitEmail +} + +func (m SubmitEmail) GetDescription() string { + return "Enter an email address to sign in or sign up." +} + +func (m SubmitEmail) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.EmailInput("email").Required(true).Preserve(true)) +} + +func (m SubmitEmail) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + _ = c.CopyInputsToStash("email") + + user, _ := models.MyUsers.FindByEmail(c.Input().Get("email").String()) + + if user != nil { + if myFlowConfig.isEnabled(FlowOptionPasswords) { + return c.ContinueFlow(StateLoginWithPassword) + } + + if !myFlowConfig.isEnabled(FlowOptionSecondFactorFlow) { + initPasscode(c, c.Stash().Get("email").String(), false) + return c.ContinueFlow(StateLoginWithPasscode) + } + + return c.ContinueFlowWithError(StateError, flowpilot.FlowDiscontinuityError) + } + + return c.ContinueFlow(StateConfirmAccountCreation) +} + +type GetWAChallenge struct{} + +func (m GetWAChallenge) GetName() flowpilot.MethodName { + return MethodGetWAChallenge +} + +func (m GetWAChallenge) GetDescription() string { + return "Get the passkey challenge." +} + +func (m GetWAChallenge) Initialize(_ flowpilot.InitializationContext) {} + +func (m GetWAChallenge) Execute(c flowpilot.ExecutionContext) error { + initPasskey(c) + return c.ContinueFlow(StateLoginWithPasskey) +} + +type VerifyWAPublicKey struct{} + +func (m VerifyWAPublicKey) GetName() flowpilot.MethodName { + return MethodVerifyWAPublicKey +} + +func (m VerifyWAPublicKey) GetDescription() string { + return "Verifies the challenge." +} + +func (m VerifyWAPublicKey) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("passkey_public_key").Required(true).CompareWithStash(true)) +} + +func (m VerifyWAPublicKey) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + return c.ContinueFlow(StateSuccess) +} + +type SubmitExistingPassword struct{} + +func (m SubmitExistingPassword) GetName() flowpilot.MethodName { + return MethodSubmitExistingPassword +} + +func (m SubmitExistingPassword) GetDescription() string { + return "Enter your password to sign in." +} + +func (m SubmitExistingPassword) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.PasswordInput("password").Required(true)) +} + +func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + email := c.Stash().Get("email").String() + user, _ := models.MyUsers.FindByEmail(email) + + if user != nil && user.Password == c.Input().Get("password").String() { + if myFlowConfig.isEnabled(FlowOptionSecondFactorFlow) && user.Passcode2faEnabled { + initPasscode(c, email, true) + return c.ContinueFlow(StateLoginWithPasscode) + } + + if user.PasskeySynced { + return c.ContinueFlow(StateSuccess) + } + + return c.ContinueFlow(StateConfirmPasskeyCreation) + } + + c.Schema().SetError("password", flowpilot.ValueInvalidError) + + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) +} + +type RequestRecovery struct{} + +func (m RequestRecovery) GetName() flowpilot.MethodName { + return MethodRequestRecovery +} + +func (m RequestRecovery) GetDescription() string { + return "Request passcode recovery to set a new password." +} + +func (m RequestRecovery) Initialize(c flowpilot.InitializationContext) { + if myFlowConfig.isEnabled(FlowOptionSecondFactorFlow) { + c.SuspendMethod() + } +} + +func (m RequestRecovery) Execute(c flowpilot.ExecutionContext) error { + initPasscode(c, c.Stash().Get("email").String(), false) + return c.ContinueFlow(StateRecoverPasswordViaPasscode) +} + +type SubmitPasscodeCode struct{} + +func (m SubmitPasscodeCode) GetName() flowpilot.MethodName { + return MethodSubmitPasscodeCode +} + +func (m SubmitPasscodeCode) GetDescription() string { + return "Enter the passcode sent via email." +} + +func (m SubmitPasscodeCode) Initialize(c flowpilot.InitializationContext) { + c.AddInputs( + flowpilot.StringInput("passcode_id").Required(true).Hidden(true).Preserve(true).CompareWithStash(true), + flowpilot.StringInput("code").Required(true).MinLength(6).MaxLength(6).CompareWithStash(true), + flowpilot.StringInput("passcode_2fa_token").Required(true).Hidden(true).Preserve(true).CompareWithStash(true).ConditionalIncludeFromStash(true), + ) +} + +func (m SubmitPasscodeCode) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + if StateRecoverPasswordViaPasscode == c.GetCurrentState() { + return c.ContinueFlow(StateUpdateExistingPassword) + } + + user, _ := models.MyUsers.FindByEmail(c.Stash().Get("email").String()) + + if StateLoginWithPasscode == c.GetCurrentState() || StateVerifyEmailViaPasscode == c.GetCurrentState() { + if user != nil && user.PasskeySynced { + return c.ContinueFlow(StateSuccess) + } + + return c.ContinueFlow(StateConfirmPasskeyCreation) + } + + return c.ContinueFlow(StateSuccess) +} + +type CreateUser struct{} + +func (m CreateUser) GetName() flowpilot.MethodName { + return MethodCreateUser +} + +func (m CreateUser) GetDescription() string { + return "Confirm account creation." +} + +func (m CreateUser) Initialize(c flowpilot.InitializationContext) { + c.AddInputs() +} + +func (m CreateUser) Execute(c flowpilot.ExecutionContext) error { + if myFlowConfig.isEnabled(FlowOptionPasswords) { + return c.ContinueFlow(StatePasswordCreation) + } + + if myFlowConfig.isEnabled(FlowOptionEmailVerification) { + initPasscode(c, c.Stash().Get("email").String(), false) + return c.ContinueFlow(StateLoginWithPasscode) + } + + return c.ContinueFlow(StateConfirmPasskeyCreation) +} + +type SubmitNewPassword struct{} + +func (m SubmitNewPassword) GetName() flowpilot.MethodName { + return MethodSubmitNewPassword +} + +func (m SubmitNewPassword) GetDescription() string { + return "Enter a new password" +} + +func (m SubmitNewPassword) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.PasswordInput("password").Required(true).MinLength(8).MaxLength(32)) +} + +func (m SubmitNewPassword) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + if StateUpdateExistingPassword == c.GetCurrentState() { + return c.ContinueFlow(StateSuccess) + } + + if myFlowConfig.isEnabled(FlowOptionEmailVerification) { + initPasscode(c, c.Stash().Get("email").String(), false) + return c.ContinueFlow(StateVerifyEmailViaPasscode) + } + + return c.ContinueFlow(StateConfirmPasskeyCreation) + +} + +type GetWAAssertion struct{} + +func (m GetWAAssertion) GetName() flowpilot.MethodName { + return MethodGetWAAssertion +} + +func (m GetWAAssertion) GetDescription() string { + return "Creates a new passkey." +} + +func (m GetWAAssertion) Initialize(_ flowpilot.InitializationContext) {} + +func (m GetWAAssertion) Execute(c flowpilot.ExecutionContext) error { + initPasskey(c) + return c.ContinueFlow(StateCreatePasskey) +} + +type VerifyWAAssertion struct{} + +func (m VerifyWAAssertion) GetName() flowpilot.MethodName { + return MethodVerifyWAAssertion +} + +func (m VerifyWAAssertion) GetDescription() string { + return "Verifies the passkey creation." +} + +func (m VerifyWAAssertion) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("passkey_public_key").Required(true).CompareWithStash(true)) +} + +func (m VerifyWAAssertion) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + } + + return c.ContinueFlow(StateSuccess) +} + +type SkipOnboarding struct{} + +func (m SkipOnboarding) GetName() flowpilot.MethodName { + return MethodSkipOnboarding +} + +func (m SkipOnboarding) GetDescription() string { + return "Skips the onboarding process." +} + +func (m SkipOnboarding) Initialize(_ flowpilot.InitializationContext) {} + +func (m SkipOnboarding) Execute(c flowpilot.ExecutionContext) error { + return c.ContinueFlow(StateSuccess) +} + +type Back struct{} + +func (m Back) GetName() flowpilot.MethodName { + return MethodBack +} + +func (m Back) GetDescription() string { + return "Go one step back." +} + +func (m Back) Initialize(_ flowpilot.InitializationContext) {} + +func (m Back) Execute(c flowpilot.ExecutionContext) error { + if previousState := c.GetPreviousState(); previousState != nil { + return c.ContinueFlow(*previousState) + } + + return c.ContinueFlow(c.GetInitialState()) +} diff --git a/backend/flow_api_test/static/generic_client.html b/backend/flow_api_test/static/generic_client.html new file mode 100644 index 000000000..3f33a75d5 --- /dev/null +++ b/backend/flow_api_test/static/generic_client.html @@ -0,0 +1,270 @@ + + + + + +

✨ Login

+ + + + + +
+ + + diff --git a/backend/flow_api_test/types.go b/backend/flow_api_test/types.go new file mode 100644 index 000000000..51965b1d4 --- /dev/null +++ b/backend/flow_api_test/types.go @@ -0,0 +1,34 @@ +package flow_api_test + +import "github.com/teamhanko/hanko/backend/flowpilot" + +const ( + StateSignInOrSignUp flowpilot.StateName = "init" + StateError flowpilot.StateName = "error" + StateSuccess flowpilot.StateName = "success" + StateConfirmAccountCreation flowpilot.StateName = "confirmation" + StateLoginWithPassword flowpilot.StateName = "login_with_password" + StateLoginWithPasscode flowpilot.StateName = "login_with_passcode" + StateLoginWithPasskey flowpilot.StateName = "login_with_passkey" + StateCreatePasskey flowpilot.StateName = "create_passkey" + StateUpdateExistingPassword flowpilot.StateName = "update_existing_password" + StateRecoverPasswordViaPasscode flowpilot.StateName = "recover_password_via_passcode" + StatePasswordCreation flowpilot.StateName = "password_creation" + StateConfirmPasskeyCreation flowpilot.StateName = "confirm_passkey_creation" + StateVerifyEmailViaPasscode flowpilot.StateName = "verify_email_via_passcode" +) + +const ( + MethodSubmitEmail flowpilot.MethodName = "submit_email" + MethodGetWAChallenge flowpilot.MethodName = "get_webauthn_challenge" + MethodVerifyWAPublicKey flowpilot.MethodName = "verify_webauthn_public_key" + MethodGetWAAssertion flowpilot.MethodName = "get_webauthn_assertion" + MethodVerifyWAAssertion flowpilot.MethodName = "verify_webauthn_assertion_response" + MethodSubmitExistingPassword flowpilot.MethodName = "submit_existing_password" + MethodSubmitNewPassword flowpilot.MethodName = "submit_new_password" + MethodRequestRecovery flowpilot.MethodName = "request_recovery" + MethodSubmitPasscodeCode flowpilot.MethodName = "submit_passcode_code" + MethodCreateUser flowpilot.MethodName = "create_user" + MethodSkipOnboarding flowpilot.MethodName = "skip_onboarding" + MethodBack flowpilot.MethodName = "back" +) diff --git a/backend/flowpilot/errors.go b/backend/flowpilot/errors.go index 5359c1f1e..9ba112f75 100644 --- a/backend/flowpilot/errors.go +++ b/backend/flowpilot/errors.go @@ -12,6 +12,7 @@ type ErrorType struct { var ( TechnicalError = &ErrorType{Code: "technical_error", Message: "Something went wrong."} FlowExpiredError = &ErrorType{Code: "flow_expired_error", Message: "The flow has expired."} + FlowDiscontinuityError = &ErrorType{Code: "flow_discontinuity_error", Message: "Thr flow can't be continued."} OperationNotPermittedError = &ErrorType{Code: "operation_not_permitted_error", Message: "The operation is not permitted."} FormDataInvalidError = &ErrorType{Code: "form_data_invalid_error", Message: "Form data invalid."} EmailInvalidError = &ErrorType{Code: "email_invalid_error", Message: "The email address is invalid."} diff --git a/backend/handler/public_router.go b/backend/handler/public_router.go index ed750d030..83f258e77 100644 --- a/backend/handler/public_router.go +++ b/backend/handler/public_router.go @@ -20,6 +20,7 @@ import ( func NewPublicRouter(cfg *config.Config, persister persistence.Persister, prometheus echo.MiddlewareFunc, authenticatorMetadata mapper.AuthenticatorMetadata) *echo.Echo { e := echo.New() + e.Renderer = template.NewTemplateRenderer() e.HideBanner = true g := e.Group("") diff --git a/backend/persistence/models/flow.go b/backend/persistence/models/flow.go new file mode 100644 index 000000000..7076ccac5 --- /dev/null +++ b/backend/persistence/models/flow.go @@ -0,0 +1,65 @@ +package models + +import ( + "github.com/teamhanko/hanko/backend/flowpilot" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// Flow is used by pop to map your flows database table to your go code. +type Flow struct { + ID uuid.UUID `json:"id" db:"id"` + CurrentState string `json:"current_state" db:"current_state"` + PreviousState *string `json:"previous_state" db:"previous_state"` + StashData string `json:"stash_data" db:"stash_data"` + Version int `json:"version" db:"version"` + Completed bool `json:"completed" db:"completed"` + ExpiresAt time.Time `json:"expires_at" db:"expires_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + // transitions transitions `json:"transitions" has_many:"transitions" order_by:"created_at desc"` +} + +func (f *Flow) ToFlowpilotModel() *flowpilot.FlowModel { + flow := flowpilot.FlowModel{ + ID: f.ID, + CurrentState: flowpilot.StateName(f.CurrentState), + StashData: f.StashData, + Version: f.Version, + Completed: f.Completed, + ExpiresAt: f.ExpiresAt, + CreatedAt: f.CreatedAt, + UpdatedAt: f.UpdatedAt, + } + + if f.PreviousState != nil { + ps := flowpilot.StateName(*f.PreviousState) + flow.PreviousState = &ps + } + + return &flow +} + +// Flows is not required by pop and may be deleted +type Flows []Flow + +// Validate gets run every time you call a "pop.validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (f *Flow) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} + +// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method. +// This method is not required and may be deleted. +func (f *Flow) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} + +// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method. +// This method is not required and may be deleted. +func (f *Flow) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} diff --git a/backend/persistence/models/flowdb.go b/backend/persistence/models/flowdb.go new file mode 100644 index 000000000..e94bcf135 --- /dev/null +++ b/backend/persistence/models/flowdb.go @@ -0,0 +1,179 @@ +package models + +import ( + "database/sql" + "errors" + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +type FlowTestUser struct { + ID string + Email string + Username string + Password string + Passcode2faEnabled bool + PasskeySynced bool +} + +type FlowTestUserList []FlowTestUser + +func (ul FlowTestUserList) FindByID(id string) (*FlowTestUser, error) { + for _, user := range ul { + if user.ID == id { + return &user, nil + } + } + return nil, fmt.Errorf("user with ID %s not found", id) +} + +func (ul FlowTestUserList) FindByEmail(email string) (*FlowTestUser, error) { + for _, user := range ul { + if user.Email == email { + return &user, nil + } + } + return nil, fmt.Errorf("user with email %s not found", email) +} + +func (ul FlowTestUserList) FindByUsername(username string) (*FlowTestUser, error) { + for _, user := range ul { + if user.Username == username { + return &user, nil + } + } + return nil, fmt.Errorf("user with username %s not found", username) +} + +var MyUsers = FlowTestUserList{ + { + ID: "a1b229c0-a1e3-44de-b770-152e18abb31c", + Email: "user1@example.com", + Username: "user1", + Password: "test", + Passcode2faEnabled: false, + PasskeySynced: false, + }, + { + ID: "26a70349-1136-4c3f-b7dd-2725e872b357", + Email: "user2@example.com", + Username: "user2", + Password: "test", + Passcode2faEnabled: true, + PasskeySynced: true, + }, +} + +type FlowDB struct { + tx *pop.Connection +} + +func NewFlowDB(tx *pop.Connection) flowpilot.FlowDB { + return FlowDB{tx} +} + +func (flowDB FlowDB) GetFlow(flowID uuid.UUID) (*flowpilot.FlowModel, error) { + flowModel := Flow{} + + err := flowDB.tx.Find(&flowModel, flowID) + if err != nil { + return nil, err + } + + return flowModel.ToFlowpilotModel(), nil +} + +func (flowDB FlowDB) CreateFlow(flowModel flowpilot.FlowModel) error { + f := Flow{ + ID: flowModel.ID, + CurrentState: string(flowModel.CurrentState), + PreviousState: nil, + StashData: flowModel.StashData, + Version: flowModel.Version, + Completed: flowModel.Completed, + ExpiresAt: flowModel.ExpiresAt, + CreatedAt: flowModel.CreatedAt, + UpdatedAt: flowModel.UpdatedAt, + } + + err := flowDB.tx.Create(&f) + if err != nil { + return err + } + + return nil +} + +func (flowDB FlowDB) UpdateFlow(flowModel flowpilot.FlowModel) error { + f := &Flow{ + ID: flowModel.ID, + CurrentState: string(flowModel.CurrentState), + StashData: flowModel.StashData, + Version: flowModel.Version, + Completed: flowModel.Completed, + ExpiresAt: flowModel.ExpiresAt, + CreatedAt: flowModel.CreatedAt, + UpdatedAt: flowModel.UpdatedAt, + } + + if ps := flowModel.PreviousState; ps != nil { + previousState := string(*ps) + f.PreviousState = &previousState + } + + previousVersion := flowModel.Version - 1 + + count, err := flowDB.tx. + Where("id = ?", f.ID). + Where("version = ?", previousVersion). + UpdateQuery(f, "current_state", "previous_state", "stash_data", "version", "completed") + if err != nil { + return err + } + + if count != 1 { + return errors.New("version conflict while updating the flow") + } + + return nil +} + +func (flowDB FlowDB) CreateTransition(transitionModel flowpilot.TransitionModel) error { + t := Transition{ + ID: transitionModel.ID, + FlowID: transitionModel.FlowID, + Method: string(transitionModel.Method), + FromState: string(transitionModel.FromState), + ToState: string(transitionModel.ToState), + InputData: transitionModel.InputData, + ErrorCode: transitionModel.ErrorCode, + CreatedAt: transitionModel.CreatedAt, + UpdatedAt: transitionModel.UpdatedAt, + } + + err := flowDB.tx.Create(&t) + if err != nil { + return err + } + + return nil +} + +func (flowDB FlowDB) FindLastTransitionWithMethod(flowID uuid.UUID, method flowpilot.MethodName) (*flowpilot.TransitionModel, error) { + var transitionModel Transition + + err := flowDB.tx.Where("flow_id = ?", flowID). + Where("method = ?", method). + Order("created_at desc"). + First(&transitionModel) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + return transitionModel.ToFlowpilotModel(), nil +} diff --git a/backend/persistence/models/transition.go b/backend/persistence/models/transition.go new file mode 100644 index 000000000..1d337a416 --- /dev/null +++ b/backend/persistence/models/transition.go @@ -0,0 +1,58 @@ +package models + +import ( + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/flowpilot" + "time" +) + +// Transition is used by pop to map your Transitions database table to your go code. +type Transition struct { + ID uuid.UUID `json:"id" db:"id"` + FlowID uuid.UUID `json:"-" db:"flow_id" ` + Method string `json:"method" db:"method"` + FromState string `json:"from_state" db:"from_state"` + ToState string `json:"to_state" db:"to_state"` + InputData string `json:"input_data" db:"input_data"` + ErrorCode *string `json:"error_code" db:"error_code"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + // flow flow `json:"flow,omitempty" belongs_to:"flow"` +} + +func (t *Transition) ToFlowpilotModel() *flowpilot.TransitionModel { + return &flowpilot.TransitionModel{ + ID: t.ID, + FlowID: t.FlowID, + Method: flowpilot.MethodName(t.Method), + FromState: flowpilot.StateName(t.FromState), + ToState: flowpilot.StateName(t.ToState), + InputData: t.InputData, + ErrorCode: t.ErrorCode, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } +} + +// Transitions is not required by pop and may be deleted +type Transitions []Transition + +// Validate gets run every time you call a "pop.validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (t *Transition) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} + +// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method. +// This method is not required and may be deleted. +func (t *Transition) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} + +// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method. +// This method is not required and may be deleted. +func (t *Transition) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) { + return validate.NewErrors(), nil +} From 8d972b2b555767e2501178c18855193115678cb8 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Mon, 14 Aug 2023 12:45:20 +0200 Subject: [PATCH 005/278] chore: correct method names in test api --- backend/flow_api_test/flow.go | 2 +- backend/flow_api_test/methods.go | 12 ++++++------ backend/flow_api_test/types.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index e62e9a78b..91c0b4e6e 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -14,7 +14,7 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StateUpdateExistingPassword, SubmitNewPassword{}). State(StateConfirmAccountCreation, CreateUser{}, Back{}). State(StatePasswordCreation, SubmitNewPassword{}). - State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipOnboarding{}). + State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipPasskeyCreation{}). State(StateCreatePasskey, VerifyWAAssertion{}). State(StateVerifyEmailViaPasscode, SubmitPasscodeCode{}). State(StateError). diff --git a/backend/flow_api_test/methods.go b/backend/flow_api_test/methods.go index 7698e0322..7daab27ff 100644 --- a/backend/flow_api_test/methods.go +++ b/backend/flow_api_test/methods.go @@ -282,19 +282,19 @@ func (m VerifyWAAssertion) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlow(StateSuccess) } -type SkipOnboarding struct{} +type SkipPasskeyCreation struct{} -func (m SkipOnboarding) GetName() flowpilot.MethodName { - return MethodSkipOnboarding +func (m SkipPasskeyCreation) GetName() flowpilot.MethodName { + return MethodSkipPasskeyCreation } -func (m SkipOnboarding) GetDescription() string { +func (m SkipPasskeyCreation) GetDescription() string { return "Skips the onboarding process." } -func (m SkipOnboarding) Initialize(_ flowpilot.InitializationContext) {} +func (m SkipPasskeyCreation) Initialize(_ flowpilot.InitializationContext) {} -func (m SkipOnboarding) Execute(c flowpilot.ExecutionContext) error { +func (m SkipPasskeyCreation) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlow(StateSuccess) } diff --git a/backend/flow_api_test/types.go b/backend/flow_api_test/types.go index 51965b1d4..53dc6063e 100644 --- a/backend/flow_api_test/types.go +++ b/backend/flow_api_test/types.go @@ -29,6 +29,6 @@ const ( MethodRequestRecovery flowpilot.MethodName = "request_recovery" MethodSubmitPasscodeCode flowpilot.MethodName = "submit_passcode_code" MethodCreateUser flowpilot.MethodName = "create_user" - MethodSkipOnboarding flowpilot.MethodName = "skip_onboarding" + MethodSkipPasskeyCreation flowpilot.MethodName = "skip_passkey_creation" MethodBack flowpilot.MethodName = "back" ) From 90ab505ed402bdbaa8e21e68a4ef0d3b93ba6e18 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Mon, 14 Aug 2023 12:47:46 +0200 Subject: [PATCH 006/278] chore: remove unused methods --- backend/flowpilot/builder.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 8dca8e084..661b94c3d 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -57,15 +57,3 @@ func (fb *FlowBuilder) Build() Flow { TTL: fb.ttl, } } - -// TransitionBuilder is a builder struct for creating a new Transition. -type TransitionBuilder struct { - method Method -} - -// Build constructs and returns the Transition object. -func (tb *TransitionBuilder) Build() *Transition { - return &Transition{ - Method: tb.method, - } -} From dcce35a290b11b766226fe5f93dd417fe53cb591 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Mon, 14 Aug 2023 18:17:25 +0200 Subject: [PATCH 007/278] chore: solved some of the todos + refactoring --- backend/flow_api_test/flow.go | 5 +- backend/flow_api_test/helper.go | 6 +- backend/flow_api_test/methods.go | 14 +- backend/flow_api_test/types.go | 1 + backend/flowpilot/context.go | 35 +--- backend/flowpilot/context_flow.go | 17 +- backend/flowpilot/context_methodExecution.go | 34 ++-- .../flowpilot/context_methodInitialization.go | 8 +- backend/flowpilot/input.go | 146 +++++++++----- backend/flowpilot/input_schema.go | 188 +++++++++--------- backend/flowpilot/response.go | 86 +++++--- 11 files changed, 299 insertions(+), 241 deletions(-) diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index 91c0b4e6e..bd1a8b907 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -9,14 +9,15 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StateSignInOrSignUp, SubmitEmail{}, GetWAChallenge{}). State(StateLoginWithPasskey, VerifyWAPublicKey{}, Back{}). State(StateLoginWithPasscode, SubmitPasscodeCode{}, Back{}). - State(StateLoginWithPassword, SubmitExistingPassword{}, RequestRecovery{}, Back{}). + State(StateLoginWithPasscode2FA, SubmitPasscodeCode{}). State(StateRecoverPasswordViaPasscode, SubmitPasscodeCode{}, Back{}). + State(StateVerifyEmailViaPasscode, SubmitPasscodeCode{}). + State(StateLoginWithPassword, SubmitExistingPassword{}, RequestRecovery{}, Back{}). State(StateUpdateExistingPassword, SubmitNewPassword{}). State(StateConfirmAccountCreation, CreateUser{}, Back{}). State(StatePasswordCreation, SubmitNewPassword{}). State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipPasskeyCreation{}). State(StateCreatePasskey, VerifyWAAssertion{}). - State(StateVerifyEmailViaPasscode, SubmitPasscodeCode{}). State(StateError). State(StateSuccess). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). diff --git a/backend/flow_api_test/helper.go b/backend/flow_api_test/helper.go index f9a0ee2d9..39ed6870d 100644 --- a/backend/flow_api_test/helper.go +++ b/backend/flow_api_test/helper.go @@ -22,9 +22,9 @@ func initPasscode(c flowpilot.ExecutionContext, email string, generate2faToken b // resend passcode id, _ := uuid.NewV4() _ = c.Stash().Set("passcode_id", id.String()) - _ = c.Flash().Set("passcode_id", id.String()) + _ = c.Input().Set("passcode_id", id.String()) } else { - _ = c.Flash().Set("passcode_id", passcodeIDStash.String()) + _ = c.Input().Set("passcode_id", passcodeIDStash.String()) } _ = c.Stash().Set("email", email) @@ -33,7 +33,7 @@ func initPasscode(c flowpilot.ExecutionContext, email string, generate2faToken b if generate2faToken { token, _ := generateToken(32) _ = c.Stash().Set("passcode_2fa_token", token) - _ = c.Flash().Set("passcode_2fa_token", token) + _ = c.Input().Set("passcode_2fa_token", token) } } diff --git a/backend/flow_api_test/methods.go b/backend/flow_api_test/methods.go index 7daab27ff..266ef0963 100644 --- a/backend/flow_api_test/methods.go +++ b/backend/flow_api_test/methods.go @@ -24,7 +24,7 @@ func (m SubmitEmail) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) } - _ = c.CopyInputsToStash("email") + _ = c.CopyInputValuesToStash("email") user, _ := models.MyUsers.FindByEmail(c.Input().Get("email").String()) @@ -108,7 +108,7 @@ func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { if user != nil && user.Password == c.Input().Get("password").String() { if myFlowConfig.isEnabled(FlowOptionSecondFactorFlow) && user.Passcode2faEnabled { initPasscode(c, email, true) - return c.ContinueFlow(StateLoginWithPasscode) + return c.ContinueFlow(StateLoginWithPasscode2FA) } if user.PasskeySynced { @@ -118,7 +118,7 @@ func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlow(StateConfirmPasskeyCreation) } - c.Schema().SetError("password", flowpilot.ValueInvalidError) + c.Input().SetError("password", flowpilot.ValueInvalidError) return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) } @@ -158,7 +158,7 @@ func (m SubmitPasscodeCode) Initialize(c flowpilot.InitializationContext) { c.AddInputs( flowpilot.StringInput("passcode_id").Required(true).Hidden(true).Preserve(true).CompareWithStash(true), flowpilot.StringInput("code").Required(true).MinLength(6).MaxLength(6).CompareWithStash(true), - flowpilot.StringInput("passcode_2fa_token").Required(true).Hidden(true).Preserve(true).CompareWithStash(true).ConditionalIncludeFromStash(true), + flowpilot.StringInput("passcode_2fa_token").Required(true).Hidden(true).Preserve(true).CompareWithStash(true).ConditionalIncludeOnState(StateLoginWithPasscode2FA), ) } @@ -167,13 +167,13 @@ func (m SubmitPasscodeCode) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) } - if StateRecoverPasswordViaPasscode == c.GetCurrentState() { + if c.CurrentStateEquals(StateRecoverPasswordViaPasscode) { return c.ContinueFlow(StateUpdateExistingPassword) } user, _ := models.MyUsers.FindByEmail(c.Stash().Get("email").String()) - if StateLoginWithPasscode == c.GetCurrentState() || StateVerifyEmailViaPasscode == c.GetCurrentState() { + if c.CurrentStateEquals(StateLoginWithPasscode, StateVerifyEmailViaPasscode, StateLoginWithPasscode2FA) { if user != nil && user.PasskeySynced { return c.ContinueFlow(StateSuccess) } @@ -230,7 +230,7 @@ func (m SubmitNewPassword) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) } - if StateUpdateExistingPassword == c.GetCurrentState() { + if c.CurrentStateEquals(StateUpdateExistingPassword) { return c.ContinueFlow(StateSuccess) } diff --git a/backend/flow_api_test/types.go b/backend/flow_api_test/types.go index 53dc6063e..526fac812 100644 --- a/backend/flow_api_test/types.go +++ b/backend/flow_api_test/types.go @@ -9,6 +9,7 @@ const ( StateConfirmAccountCreation flowpilot.StateName = "confirmation" StateLoginWithPassword flowpilot.StateName = "login_with_password" StateLoginWithPasscode flowpilot.StateName = "login_with_passcode" + StateLoginWithPasscode2FA flowpilot.StateName = "login_with_passcode_2fa" StateLoginWithPasskey flowpilot.StateName = "login_with_passkey" StateCreatePasskey flowpilot.StateName = "create_passkey" StateUpdateExistingPassword flowpilot.StateName = "update_existing_password" diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 68cc9e95b..1103f6a26 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -20,15 +20,12 @@ type flowContext interface { Payload() jsonmanager.JSONManager // Stash returns the JSONManager for accessing stash data. Stash() jsonmanager.JSONManager - - // TODO: Maybe we should remove 'Flash' and provide the same functionality under "Input()" - - // Flash returns the JSONManager for accessing flash data. - Flash() jsonmanager.JSONManager // GetInitialState returns the initial state of the Flow. GetInitialState() StateName // GetCurrentState returns the current state of the Flow. GetCurrentState() StateName + // CurrentStateEquals returns true, when one of the given states matches the current state. + CurrentStateEquals(states ...StateName) bool // GetPreviousState returns the previous state of the Flow. GetPreviousState() *StateName // GetErrorState returns the designated error state of the Flow. @@ -42,7 +39,7 @@ type flowContext interface { // methodInitializationContext represents the basic context for a Flow method's initialization. type methodInitializationContext interface { // AddInputs adds input parameters to the schema. - AddInputs(inputList ...*DefaultInput) *DefaultSchema + AddInputs(inputList ...Input) // Stash returns the ReadOnlyJSONManager for accessing stash data. Stash() jsonmanager.ReadOnlyJSONManager // SuspendMethod suspends the current method's execution. @@ -51,10 +48,9 @@ type methodInitializationContext interface { // methodExecutionContext represents the context for a method execution. type methodExecutionContext interface { - // Input returns the ReadOnlyJSONManager for accessing input data. - Input() jsonmanager.ReadOnlyJSONManager - // Schema returns the MethodExecutionSchema for the method. - Schema() MethodExecutionSchema + flowContext + // Input returns the MethodExecutionSchema for the method. + Input() MethodExecutionSchema // TODO: FetchMethodInput (for a method name) is maybe useless and can be removed or replaced. @@ -62,16 +58,12 @@ type methodExecutionContext interface { FetchMethodInput(methodName MethodName) (jsonmanager.ReadOnlyJSONManager, error) // ValidateInputData validates the input data against the schema. ValidateInputData() bool - - // TODO: CopyInputsToStash can maybe removed or replaced with an option you can set via the input schema. - - // CopyInputsToStash copies specified inputs to the stash. - CopyInputsToStash(inputNames ...string) error + // CopyInputValuesToStash copies specified inputs to the stash. + CopyInputValuesToStash(inputNames ...string) error } // methodExecutionContinuationContext represents the context within a method continuation. type methodExecutionContinuationContext interface { - flowContext methodExecutionContext // ContinueFlow continues the Flow execution to the specified next state. ContinueFlow(nextState StateName) error @@ -117,7 +109,6 @@ func createAndInitializeFlow(db FlowDB, flow Flow) (*Response, error) { stash := jsonmanager.NewJSONManager() payload := jsonmanager.NewJSONManager() - flash := jsonmanager.NewJSONManager() // Create a new Flow model with the provided parameters. p := flowCreationParam{currentState: flow.InitialState, expiresAt: expiresAt} @@ -133,7 +124,6 @@ func createAndInitializeFlow(db FlowDB, flow Flow) (*Response, error) { flowModel: *flowModel, stash: stash, payload: payload, - flash: flash, } // Generate a response based on the execution result. @@ -171,7 +161,6 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Initialize JSONManagers for payload and flash data. payload := jsonmanager.NewJSONManager() - flash := jsonmanager.NewJSONManager() // Create a defaultFlowContext instance. fc := defaultFlowContext{ @@ -180,7 +169,6 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res flowModel: *flowModel, stash: stash, payload: payload, - flash: flash, } // Get the available transitions for the current state. @@ -205,8 +193,8 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res } // Initialize the schema and method context for method execution. - schema := DefaultSchema{} - mic := defaultMethodInitializationContext{schema: &schema, stash: stash} + schema := newSchemaWithInputData(inputJSON) + mic := defaultMethodInitializationContext{schema: schema.toInitializationSchema(), stash: stash} method.Initialize(&mic) // Check if the method is suspended. @@ -216,9 +204,8 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Create a methodExecutionContext instance for method execution. mec := defaultMethodExecutionContext{ - schema: &schema, methodName: methodName, - input: inputJSON, + input: schema, defaultFlowContext: fc, } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index f10328d71..45bf8bea5 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -10,7 +10,6 @@ import ( type defaultFlowContext struct { payload jsonmanager.JSONManager // JSONManager for payload data. stash jsonmanager.JSONManager // JSONManager for stash data. - flash jsonmanager.JSONManager // JSONManager for flash data. flow Flow // The associated Flow instance. dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. flowModel FlowModel // The current FlowModel. @@ -36,6 +35,17 @@ func (fc *defaultFlowContext) GetCurrentState() StateName { return fc.flowModel.CurrentState } +// CurrentStateEquals returns true, when one of the given stateNames matches the current state name. +func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { + for _, s := range stateNames { + if s == fc.flowModel.CurrentState { + return true + } + } + + return false +} + // GetPreviousState returns a pointer to the previous state of the Flow. func (fc *defaultFlowContext) GetPreviousState() *StateName { return fc.flowModel.PreviousState @@ -56,11 +66,6 @@ func (fc *defaultFlowContext) Stash() jsonmanager.JSONManager { return fc.stash } -// Flash returns the JSONManager for accessing flash data. -func (fc *defaultFlowContext) Flash() jsonmanager.JSONManager { - return fc.flash -} - // StateExists checks if a given state exists within the Flow. func (fc *defaultFlowContext) StateExists(stateName StateName) bool { return fc.flow.stateExists(stateName) diff --git a/backend/flowpilot/context_methodExecution.go b/backend/flowpilot/context_methodExecution.go index 67bf49290..a2f81e968 100644 --- a/backend/flowpilot/context_methodExecution.go +++ b/backend/flowpilot/context_methodExecution.go @@ -8,10 +8,9 @@ import ( // defaultMethodExecutionContext is the default implementation of the methodExecutionContext interface. type defaultMethodExecutionContext struct { - methodName MethodName // Name of the method being executed. - input jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing input data. - schema MethodExecutionSchema // Schema for the method execution. - methodResult *executionResult // Result of the method execution. + methodName MethodName // Name of the method being executed. + input MethodExecutionSchema // JSONManager for accessing input data. + methodResult *executionResult // Result of the method execution. defaultFlowContext // Embedding the defaultFlowContext for common context fields. } @@ -49,11 +48,8 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio mec.flowModel = *flowModel - // Get transition data from the response schema for recording. - inputData, err := mec.schema.toResponseSchema().getTransitionData(mec.input) - if err != nil { - return fmt.Errorf("failed to get data from response schema: %w", err) - } + // Get inputDataToPersist from the executed method's schema for recording. + inputDataToPersist := mec.input.getDataToPersist() // Prepare parameters for creating a new Transition in the database. transitionCreationParam := transitionCreationParam{ @@ -61,7 +57,7 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio methodName: mec.methodName, fromState: currentState, toState: executionResult.nextState, - inputData: inputData.String(), + inputData: inputDataToPersist.String(), errType: executionResult.errType, } @@ -87,8 +83,7 @@ func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, errT errType: errType, methodExecutionResult: &methodExecutionResult{ methodName: mec.methodName, - schema: mec.schema.toResponseSchema(), - inputData: mec.input, + schema: mec.input, }, } @@ -103,8 +98,8 @@ func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, errT return nil } -// Input returns the ReadOnlyJSONManager for accessing input data. -func (mec *defaultMethodExecutionContext) Input() jsonmanager.ReadOnlyJSONManager { +// Input returns the MethodExecutionSchema for accessing input data. +func (mec *defaultMethodExecutionContext) Input() MethodExecutionSchema { return mec.input } @@ -113,13 +108,8 @@ func (mec *defaultMethodExecutionContext) Payload() jsonmanager.JSONManager { return mec.payload } -// Schema returns the MethodExecutionSchema for the method. -func (mec *defaultMethodExecutionContext) Schema() MethodExecutionSchema { - return mec.schema -} - -// CopyInputsToStash copies specified inputs to the stash. -func (mec *defaultMethodExecutionContext) CopyInputsToStash(inputNames ...string) error { +// CopyInputValuesToStash copies specified inputs to the stash. +func (mec *defaultMethodExecutionContext) CopyInputValuesToStash(inputNames ...string) error { for _, inputName := range inputNames { // Copy input values to the stash. err := mec.stash.Set(inputName, mec.input.Get(inputName).Value()) @@ -132,7 +122,7 @@ func (mec *defaultMethodExecutionContext) CopyInputsToStash(inputNames ...string // ValidateInputData validates the input data against the schema. func (mec *defaultMethodExecutionContext) ValidateInputData() bool { - return mec.Schema().toResponseSchema().validateInputData(mec.input, mec.stash) + return mec.input.validateInputData(mec.flowModel.CurrentState, mec.stash) } // ContinueFlow continues the Flow execution to the specified nextState. diff --git a/backend/flowpilot/context_methodInitialization.go b/backend/flowpilot/context_methodInitialization.go index 95ce591da..2da6a8517 100644 --- a/backend/flowpilot/context_methodInitialization.go +++ b/backend/flowpilot/context_methodInitialization.go @@ -4,14 +4,14 @@ import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" // defaultMethodInitializationContext is the default implementation of the methodInitializationContext interface. type defaultMethodInitializationContext struct { - schema Schema // Schema for method initialization. + schema InitializationSchema // InitializationSchema for method initialization. isSuspended bool // Flag indicating if the method is suspended. stash jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing stash data. } -// AddInputs adds input data to the Schema and returns a DefaultSchema instance. -func (mic *defaultMethodInitializationContext) AddInputs(inputList ...*DefaultInput) *DefaultSchema { - return mic.schema.AddInputs(inputList...) +// AddInputs adds input data to the InitializationSchema and returns a defaultSchema instance. +func (mic *defaultMethodInitializationContext) AddInputs(inputList ...Input) { + mic.schema.AddInputs(inputList...) } // SuspendMethod sets the isSuspended flag to indicate the method is suspended. diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go index d2ac1cf47..7c3640fce 100644 --- a/backend/flowpilot/input.go +++ b/backend/flowpilot/input.go @@ -1,6 +1,9 @@ package flowpilot -import "regexp" +import ( + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "regexp" +) // InputType represents the type of the input field. type InputType string @@ -16,32 +19,31 @@ const ( // Input defines the interface for input fields. type Input interface { - MinLength(minLength int) *DefaultInput - MaxLength(maxLength int) *DefaultInput - Required(b bool) *DefaultInput - Hidden(b bool) *DefaultInput - Preserve(b bool) *DefaultInput - Persist(b bool) *DefaultInput - - // TODO: After experimenting with 'ConditionalIncludeFromStash', I realized that it should be replaced with another - // function 'ConditionalIncludeOnStates(...StateName)'. This function would include the input field when executed - // under specific states. The issue with 'ConditionalIncludeFromStash' is, that if a method "forgets" to set the - // value beforehand, the validation won't require the value, since it's not present in the stash. In contrast, using - // 'ConditionalIncludeOnStates(...)' makes it clear that the value must be present when the current state matches - // and the decision about whether to validate the value or not isn't dependent on a different method. - - ConditionalIncludeFromStash(b bool) *DefaultInput - CompareWithStash(b bool) *DefaultInput - setValue(value interface{}) *DefaultInput - validate(value *string) bool + MinLength(minLength int) Input + MaxLength(maxLength int) Input + Required(b bool) Input + Hidden(b bool) Input + Preserve(b bool) Input + Persist(b bool) Input + ConditionalIncludeOnState(states ...StateName) Input + CompareWithStash(b bool) Input + + setValue(value interface{}) Input + setError(errType *ErrorType) + getName() string + shouldPersist() bool + shouldPreserve() bool + isIncludedOnState(stateName StateName) bool + validate(stateName StateName, inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.JSONManager) bool + toPublicInput() *PublicInput } // defaultExtraInputOptions holds additional input field options. type defaultExtraInputOptions struct { - preserveValue bool - persistValue bool - conditionalIncludeFromStash bool - compareWithStash bool + preserveValue bool + persistValue bool + includeOnStates []StateName + compareWithStash bool } // DefaultInput represents an input field with its options. @@ -54,6 +56,7 @@ type DefaultInput struct { required *bool hidden *bool errorType *ErrorType + defaultExtraInputOptions } @@ -70,106 +73,151 @@ type PublicInput struct { } // newInput creates a new DefaultInput instance with provided parameters. -func newInput(name string, t InputType, persistValue bool) *DefaultInput { +func newInput(name string, t InputType, persistValue bool) Input { return &DefaultInput{ name: name, dataType: t, defaultExtraInputOptions: defaultExtraInputOptions{ - preserveValue: false, - persistValue: persistValue, - conditionalIncludeFromStash: false, - compareWithStash: false, + preserveValue: false, + persistValue: persistValue, + includeOnStates: []StateName{}, + compareWithStash: false, }, } } // StringInput creates a new input field of string type. -func StringInput(name string) *DefaultInput { +func StringInput(name string) Input { return newInput(name, StringType, true) } // EmailInput creates a new input field of email type. -func EmailInput(name string) *DefaultInput { +func EmailInput(name string) Input { return newInput(name, EmailType, true) } // NumberInput creates a new input field of number type. -func NumberInput(name string) *DefaultInput { +func NumberInput(name string) Input { return newInput(name, NumberType, true) } // PasswordInput creates a new input field of password type. -func PasswordInput(name string) *DefaultInput { +func PasswordInput(name string) Input { return newInput(name, PasswordType, false) } // JSONInput creates a new input field of JSON type. -func JSONInput(name string) *DefaultInput { +func JSONInput(name string) Input { return newInput(name, JSONType, false) } // MinLength sets the minimum length for the input field. -func (i *DefaultInput) MinLength(minLength int) *DefaultInput { +func (i *DefaultInput) MinLength(minLength int) Input { i.minLength = &minLength return i } // MaxLength sets the maximum length for the input field. -func (i *DefaultInput) MaxLength(maxLength int) *DefaultInput { +func (i *DefaultInput) MaxLength(maxLength int) Input { i.maxLength = &maxLength return i } // Required sets whether the input field is required. -func (i *DefaultInput) Required(b bool) *DefaultInput { +func (i *DefaultInput) Required(b bool) Input { i.required = &b return i } // Hidden sets whether the input field is hidden. -func (i *DefaultInput) Hidden(b bool) *DefaultInput { +func (i *DefaultInput) Hidden(b bool) Input { i.hidden = &b return i } // Preserve sets whether the input field value should be preserved, so that the value is included in the response // instead of being blanked out. -func (i *DefaultInput) Preserve(b bool) *DefaultInput { +func (i *DefaultInput) Preserve(b bool) Input { i.preserveValue = b return i } // Persist sets whether the input field value should be persisted. -func (i *DefaultInput) Persist(b bool) *DefaultInput { +func (i *DefaultInput) Persist(b bool) Input { i.persistValue = b return i } -// ConditionalIncludeFromStash sets whether the input field is conditionally included from the stash. -func (i *DefaultInput) ConditionalIncludeFromStash(b bool) *DefaultInput { - i.conditionalIncludeFromStash = b +// ConditionalIncludeOnState sets the states where the input field is included. +func (i *DefaultInput) ConditionalIncludeOnState(stateNames ...StateName) Input { + i.includeOnStates = stateNames return i } +// isIncludedOnState check if a conditional input field is included according to the given stateName. +func (i *DefaultInput) isIncludedOnState(stateName StateName) bool { + if len(i.includeOnStates) == 0 { + return true + } + + for _, s := range i.includeOnStates { + if s == stateName { + return true + } + } + + return false +} + // CompareWithStash sets whether the input field is compared with stash values. -func (i *DefaultInput) CompareWithStash(b bool) *DefaultInput { +func (i *DefaultInput) CompareWithStash(b bool) Input { i.compareWithStash = b return i } -// setValue sets the value for the input field. -func (i *DefaultInput) setValue(value interface{}) *DefaultInput { +// setValue sets the value for the input field for the current response. +func (i *DefaultInput) setValue(value interface{}) Input { i.value = &value return i } -// validate performs validation on the input field. -func (i *DefaultInput) validate(inputValue *string, stashValue *string) bool { - // Validate based on input field options. +// getName returns the name of the input field. +func (i *DefaultInput) getName() string { + return i.name +} +// setError sets an error to the given input field. +func (i *DefaultInput) setError(errType *ErrorType) { + i.errorType = errType +} + +// shouldPersist indicates the value should be persisted. +func (i *DefaultInput) shouldPersist() bool { + return i.persistValue +} + +// shouldPersist indicates the value should be preserved. +func (i *DefaultInput) shouldPreserve() bool { + return i.preserveValue +} + +// validate performs validation on the input field. +func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.JSONManager) bool { // TODO: Replace with more structured validation logic. - if i.conditionalIncludeFromStash && stashValue == nil { + var inputValue *string + var stashValue *string + + if v := inputData.Get(i.name); v.Exists() { + inputValue = &v.Str + } + + if v := stashData.Get(i.name); v.Exists() { + stashValue = &v.Str + } + + if len(i.includeOnStates) > 0 && !i.isIncludedOnState(stateName) { + // skip validation return true } diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index 33efc7014..2ee6ed7aa 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -1,61 +1,96 @@ package flowpilot import ( - "fmt" "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/tidwall/gjson" ) -// Schema represents an interface for managing input data schemas. -type Schema interface { - AddInputs(inputList ...*DefaultInput) *DefaultSchema +// InitializationSchema represents an interface for managing input data schemas. +type InitializationSchema interface { + AddInputs(inputList ...Input) } // MethodExecutionSchema represents an interface for managing method execution schemas. type MethodExecutionSchema interface { + Get(path string) gjson.Result + Set(path string, value interface{}) error SetError(inputName string, errType *ErrorType) - toResponseSchema() ResponseSchema + + getInput(name string) Input + getOutputData() jsonmanager.ReadOnlyJSONManager + getDataToPersist() jsonmanager.ReadOnlyJSONManager + validateInputData(stateName StateName, stash jsonmanager.JSONManager) bool + toInitializationSchema() InitializationSchema + toPublicSchema(stateName StateName) PublicSchema +} + +// inputs represents a collection of Input instances. +type inputs []Input + +// PublicSchema represents a collection of PublicInput instances. +type PublicSchema []*PublicInput + +// defaultSchema implements the InitializationSchema interface and holds a collection of input fields. +type defaultSchema struct { + inputs + inputData jsonmanager.ReadOnlyJSONManager + outputData jsonmanager.JSONManager } -// ResponseSchema represents an interface for response schemas. -type ResponseSchema interface { - GetInput(name string) *DefaultInput - preserveInputData(inputData jsonmanager.ReadOnlyJSONManager) - getTransitionData(inputData jsonmanager.ReadOnlyJSONManager) (jsonmanager.ReadOnlyJSONManager, error) - applyFlash(flashData jsonmanager.ReadOnlyJSONManager) - applyStash(stashData jsonmanager.ReadOnlyJSONManager) - validateInputData(inputData jsonmanager.ReadOnlyJSONManager, stash jsonmanager.ReadOnlyJSONManager) bool - toPublicInputs() PublicInputs +// newSchemaWithInputData creates a new MethodExecutionSchema with input data. +func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) MethodExecutionSchema { + outputData := jsonmanager.NewJSONManager() + return &defaultSchema{ + inputData: inputData, + outputData: outputData, + } } -// Inputs represents a collection of DefaultInput instances. -type Inputs []*DefaultInput +// newSchemaWithInputData creates a new MethodExecutionSchema with input data. +func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (MethodExecutionSchema, error) { + data, err := jsonmanager.NewJSONManagerFromString(outputData.String()) + if err != nil { + return nil, err + } -// PublicInputs represents a collection of PublicInput instances. -type PublicInputs []*PublicInput + return &defaultSchema{ + inputData: data, + outputData: data, + }, nil +} -// DefaultSchema implements the Schema interface and holds a collection of input fields. -type DefaultSchema struct { - Inputs +// newSchema creates a new MethodExecutionSchema with no input data. +func newSchema() MethodExecutionSchema { + inputData := jsonmanager.NewJSONManager() + return newSchemaWithInputData(inputData) } -// toResponseSchema converts the DefaultSchema to a ResponseSchema. -func (s *DefaultSchema) toResponseSchema() ResponseSchema { +// toInitializationSchema converts MethodExecutionSchema to InitializationSchema. +func (s *defaultSchema) toInitializationSchema() InitializationSchema { return s } -// AddInputs adds input fields to the DefaultSchema and returns the updated schema. -func (s *DefaultSchema) AddInputs(inputList ...*DefaultInput) *DefaultSchema { +// Get retrieves a value at the specified path in the input data. +func (s *defaultSchema) Get(path string) gjson.Result { + return s.inputData.Get(path) +} + +// Set updates the JSON data at the specified path with the provided value. +func (s *defaultSchema) Set(path string, value interface{}) error { + return s.outputData.Set(path, value) +} + +// AddInputs adds input fields to the defaultSchema and returns the updated schema. +func (s *defaultSchema) AddInputs(inputList ...Input) { for _, i := range inputList { - s.Inputs = append(s.Inputs, i) + s.inputs = append(s.inputs, i) } - - return s } -// GetInput retrieves an input field from the schema based on its name. -func (s *DefaultSchema) GetInput(name string) *DefaultInput { - for _, i := range s.Inputs { - if i.name == name { +// getInput retrieves an input field from the schema based on its name. +func (s *defaultSchema) getInput(name string) Input { + for _, i := range s.inputs { + if i.getName() == name { return i } } @@ -64,29 +99,18 @@ func (s *DefaultSchema) GetInput(name string) *DefaultInput { } // SetError sets an error type for an input field in the schema. -func (s *DefaultSchema) SetError(inputName string, errType *ErrorType) { - if i := s.GetInput(inputName); i != nil { - i.errorType = errType +func (s *defaultSchema) SetError(inputName string, errType *ErrorType) { + if i := s.getInput(inputName); i != nil { + i.setError(errType) } } // validateInputData validates the input data based on the input definitions in the schema. -func (s *DefaultSchema) validateInputData(inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.ReadOnlyJSONManager) bool { +func (s *defaultSchema) validateInputData(stateName StateName, stash jsonmanager.JSONManager) bool { valid := true - for _, i := range s.Inputs { - var inputValue *string - var stashValue *string - - if v := inputData.Get(i.name); v.Exists() { - inputValue = &v.Str - } - - if v := stashData.Get(i.name); v.Exists() { - stashValue = &v.Str - } - - if !i.validate(inputValue, stashValue) && valid { + for _, i := range s.inputs { + if !i.validate(stateName, s.inputData, stash) && valid { valid = false } } @@ -94,62 +118,44 @@ func (s *DefaultSchema) validateInputData(inputData jsonmanager.ReadOnlyJSONMana return valid } -// preserveInputData preserves input data by setting values of inputs that should be preserved. -func (s *DefaultSchema) preserveInputData(inputData jsonmanager.ReadOnlyJSONManager) { - for _, i := range s.Inputs { - if v := inputData.Get(i.name); v.Exists() { - if i.preserveValue { - i.setValue(v.Str) - } - } - } -} - -// getTransitionData filters input data to persist based on schema definitions. -func (s *DefaultSchema) getTransitionData(inputData jsonmanager.ReadOnlyJSONManager) (jsonmanager.ReadOnlyJSONManager, error) { +// getDataToPersist filters and returns data that should be persisted based on schema definitions. +func (s *defaultSchema) getDataToPersist() jsonmanager.ReadOnlyJSONManager { toPersist := jsonmanager.NewJSONManager() - for _, i := range s.Inputs { - if v := inputData.Get(i.name); v.Exists() && i.persistValue { - if err := toPersist.Set(i.name, v.Value()); err != nil { - return nil, fmt.Errorf("failed to copy data: %v", err) - } + for _, i := range s.inputs { + if v := s.inputData.Get(i.getName()); v.Exists() && i.shouldPersist() { + _ = toPersist.Set(i.getName(), v.Value()) } } - return toPersist, nil + return toPersist } -// applyFlash updates input values in the schema with corresponding values from flash data. -func (s *DefaultSchema) applyFlash(flashData jsonmanager.ReadOnlyJSONManager) { - for _, i := range s.Inputs { - v := flashData.Get(i.name) - - if v.Exists() { - i.setValue(v.Value()) - } - } +// getOutputData returns the output data from the schema. +func (s *defaultSchema) getOutputData() jsonmanager.ReadOnlyJSONManager { + return s.outputData } -// applyStash updates input values in the schema with corresponding values from stash data. -func (s *DefaultSchema) applyStash(stashData jsonmanager.ReadOnlyJSONManager) { - n := 0 +// toPublicSchema converts defaultSchema to PublicSchema for public exposure. +func (s *defaultSchema) toPublicSchema(stateName StateName) PublicSchema { + var pi PublicSchema - for _, i := range s.Inputs { - if !i.conditionalIncludeFromStash || stashData.Get(i.name).Exists() { - s.Inputs[n] = i - n++ + for _, i := range s.inputs { + if !i.isIncludedOnState(stateName) { + continue } - } - s.Inputs = s.Inputs[:n] -} + outputValue := s.outputData.Get(i.getName()) + inputValue := s.inputData.Get(i.getName()) -// toPublicInputs converts DefaultSchema to PublicInputs for public exposure. -func (s *DefaultSchema) toPublicInputs() PublicInputs { - var pi PublicInputs + if outputValue.Exists() { + i.setValue(outputValue.Value()) + } + + if i.shouldPreserve() && inputValue.Exists() && !outputValue.Exists() { + i.setValue(inputValue.Value()) + } - for _, i := range s.Inputs { pi = append(pi, i.toPublicInput()) } diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index e89da8b4b..32b217523 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -2,14 +2,13 @@ package flowpilot import ( "fmt" - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // Link represents a link to an action. type Link struct { Href string `json:"href"` - Inputs PublicInputs `json:"inputs"` + Inputs PublicSchema `json:"inputs"` MethodName MethodName `json:"method_name"` Description string `json:"description"` } @@ -33,8 +32,7 @@ type Response struct { // methodExecutionResult holds the result of a method execution. type methodExecutionResult struct { methodName MethodName - schema ResponseSchema - inputData jsonmanager.ReadOnlyJSONManager + schema MethodExecutionSchema } // executionResult holds the result of an action execution. @@ -47,8 +45,24 @@ type executionResult struct { // generateResponse generates a response based on the execution result. func (er *executionResult) generateResponse(fc defaultFlowContext) (*Response, error) { + // Generate links for the response. + links := er.generateLinks(fc) + + // Create the response object. + resp := &Response{ + State: er.nextState, + Payload: fc.payload.Unmarshal(), + Links: links, + Error: er.errType, + } + return resp, nil +} + +// generateLinks generates a collection of links based on the execution result. +func (er *executionResult) generateLinks(fc defaultFlowContext) Links { var links Links + // Get transitions for the next state. transitions := fc.flow.getTransitionsForState(er.nextState) if transitions != nil { @@ -56,29 +70,21 @@ func (er *executionResult) generateResponse(fc defaultFlowContext) (*Response, e currentMethodName := t.Method.GetName() currentDescription := t.Method.GetDescription() + // Create link HREF based on the current flow context and method name. href := er.createHref(fc, currentMethodName) + schema := er.getExecutionSchema(currentMethodName) - var schema ResponseSchema - - if schema = er.getExecutionSchema(currentMethodName); schema == nil { - defaultSchema := DefaultSchema{} - mic := defaultMethodInitializationContext{schema: &defaultSchema, stash: fc.stash} - - t.Method.Initialize(&mic) - - if mic.isSuspended { + if schema == nil { + // Create schema if not available. + if schema = er.createSchema(fc, t.Method); schema == nil { continue } - - schema = &defaultSchema } - schema.applyFlash(fc.flash) - schema.applyStash(fc.stash) - + // Create the link instance. link := Link{ Href: href, - Inputs: schema.toPublicInputs(), + Inputs: schema.toPublicSchema(er.nextState), MethodName: currentMethodName, Description: currentDescription, } @@ -87,29 +93,43 @@ func (er *executionResult) generateResponse(fc defaultFlowContext) (*Response, e } } - resp := &Response{ - State: er.nextState, - Payload: fc.payload.Unmarshal(), - Links: links, - Error: er.errType, + return links +} + +// createSchema creates an execution schema for a method if needed. +func (er *executionResult) createSchema(fc defaultFlowContext, method Method) MethodExecutionSchema { + var schema MethodExecutionSchema + var err error + + if er.methodExecutionResult != nil { + data := er.methodExecutionResult.schema.getOutputData() + schema, err = newSchemaWithOutputData(data) + } else { + schema = newSchema() } - return resp, nil + if err != nil { + return nil + } + + // Initialize the method. + mic := defaultMethodInitializationContext{schema: schema.toInitializationSchema(), stash: fc.stash} + method.Initialize(&mic) + + if mic.isSuspended { + return nil + } + + return schema } // getExecutionSchema gets the execution schema for a given method name. -func (er *executionResult) getExecutionSchema(methodName MethodName) ResponseSchema { +func (er *executionResult) getExecutionSchema(methodName MethodName) MethodExecutionSchema { if er.methodExecutionResult == nil || methodName != er.methodExecutionResult.methodName { - // The current method result does not belong to the methodName. return nil } - schema := er.methodExecutionResult.schema - inputData := er.methodExecutionResult.inputData - - schema.preserveInputData(inputData) - - return schema + return er.methodExecutionResult.schema } // createHref creates a link HREF based on the current flow context and method name. From 366d786070f5dea9d1fd37874ddc62eb43569ab6 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 31 Aug 2023 00:19:07 +0200 Subject: [PATCH 008/278] feat: improved interfaces for errors and flow execution results --- backend/flow_api_test/echo.go | 21 ++- backend/flow_api_test/flow.go | 1 + backend/flow_api_test/methods.go | 19 +- backend/flowpilot/builder.go | 8 + backend/flowpilot/context.go | 32 ++-- backend/flowpilot/context_methodExecution.go | 16 +- backend/flowpilot/db.go | 7 +- backend/flowpilot/errors.go | 172 +++++++++++++++++-- backend/flowpilot/flow.go | 20 ++- backend/flowpilot/input.go | 39 ++--- backend/flowpilot/input_schema.go | 8 +- backend/flowpilot/response.go | 90 ++++++++-- 12 files changed, 330 insertions(+), 103 deletions(-) diff --git a/backend/flow_api_test/echo.go b/backend/flow_api_test/echo.go index 8c00ecfe9..2ef0acb7f 100644 --- a/backend/flow_api_test/echo.go +++ b/backend/flow_api_test/echo.go @@ -7,7 +7,6 @@ import ( "github.com/teamhanko/hanko/backend/flowpilot" "github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/persistence/models" - "net/http" ) type FlowPilotHandler struct { @@ -32,18 +31,26 @@ func (e *FlowPilotHandler) LoginFlowHandler(c echo.Context) error { myFlowConfig = flowConfig - return e.Persister.Transaction(func(tx *pop.Connection) error { + err := e.Persister.Transaction(func(tx *pop.Connection) error { db := models.NewFlowDB(tx) - flowResponse, err := Flow.Execute(db, + result, flowpilotErr := Flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(body.InputData)) - if err != nil { - c.Logger().Errorf("flowpilot error: %w", err) - return c.JSON(http.StatusOK, Flow.ErrorResponse()) + if flowpilotErr != nil { + return flowpilotErr } - return c.JSON(http.StatusOK, flowResponse) + return c.JSON(result.Status(), result.Response()) }) + + if err != nil { + c.Logger().Errorf("tx error: %v", err) + result := Flow.ResultFromError(err) + + return c.JSON(result.Status(), result.Response()) + } + + return nil } diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index bd1a8b907..1710fc81c 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -22,4 +22,5 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StateSuccess). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). TTL(time.Minute * 10). + Debug(true). Build() diff --git a/backend/flow_api_test/methods.go b/backend/flow_api_test/methods.go index 266ef0963..5bba7cb42 100644 --- a/backend/flow_api_test/methods.go +++ b/backend/flow_api_test/methods.go @@ -1,6 +1,7 @@ package flow_api_test import ( + "errors" "github.com/teamhanko/hanko/backend/flowpilot" "github.com/teamhanko/hanko/backend/persistence/models" ) @@ -21,7 +22,7 @@ func (m SubmitEmail) Initialize(c flowpilot.InitializationContext) { func (m SubmitEmail) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } _ = c.CopyInputValuesToStash("email") @@ -38,7 +39,7 @@ func (m SubmitEmail) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlow(StateLoginWithPasscode) } - return c.ContinueFlowWithError(StateError, flowpilot.FlowDiscontinuityError) + return c.ContinueFlowWithError(StateError, flowpilot.ErrorFlowDiscontinuity) } return c.ContinueFlow(StateConfirmAccountCreation) @@ -77,7 +78,7 @@ func (m VerifyWAPublicKey) Initialize(c flowpilot.InitializationContext) { func (m VerifyWAPublicKey) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } return c.ContinueFlow(StateSuccess) @@ -99,7 +100,7 @@ func (m SubmitExistingPassword) Initialize(c flowpilot.InitializationContext) { func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } email := c.Stash().Get("email").String() @@ -118,9 +119,9 @@ func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { return c.ContinueFlow(StateConfirmPasskeyCreation) } - c.Input().SetError("password", flowpilot.ValueInvalidError) + c.Input().SetError("password", flowpilot.ErrorValueInvalid.Wrap(errors.New("password does not match"))) - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } type RequestRecovery struct{} @@ -164,7 +165,7 @@ func (m SubmitPasscodeCode) Initialize(c flowpilot.InitializationContext) { func (m SubmitPasscodeCode) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } if c.CurrentStateEquals(StateRecoverPasswordViaPasscode) { @@ -227,7 +228,7 @@ func (m SubmitNewPassword) Initialize(c flowpilot.InitializationContext) { func (m SubmitNewPassword) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } if c.CurrentStateEquals(StateUpdateExistingPassword) { @@ -276,7 +277,7 @@ func (m VerifyWAAssertion) Initialize(c flowpilot.InitializationContext) { func (m VerifyWAAssertion) Execute(c flowpilot.ExecutionContext) error { if valid := c.ValidateInputData(); !valid { - return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.FormDataInvalidError) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) } return c.ContinueFlow(StateSuccess) diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 661b94c3d..980ac1406 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -12,6 +12,7 @@ type FlowBuilder struct { errorState StateName endState StateName flow StateTransitions + debug bool } // NewFlow creates a new FlowBuilder that builds a new flow available under the specified path. @@ -46,6 +47,12 @@ func (fb *FlowBuilder) FixedStates(initialState, errorState, finalState StateNam return fb } +// Debug enables the debug mode, which causes the flow response to contain the actual error. +func (fb *FlowBuilder) Debug(enabled bool) *FlowBuilder { + fb.debug = enabled + return fb +} + // Build constructs and returns the Flow object. func (fb *FlowBuilder) Build() Flow { return Flow{ @@ -55,5 +62,6 @@ func (fb *FlowBuilder) Build() Flow { ErrorState: fb.errorState, EndState: fb.endState, TTL: fb.ttl, + Debug: fb.debug, } } diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 1103f6a26..494ea1366 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -68,7 +68,7 @@ type methodExecutionContinuationContext interface { // ContinueFlow continues the Flow execution to the specified next state. ContinueFlow(nextState StateName) error // ContinueFlowWithError continues the Flow execution to the specified next state with an error. - ContinueFlowWithError(nextState StateName, errType *ErrorType) error + ContinueFlowWithError(nextState StateName, flowErr FlowError) error // TODO: Implement a function to step back to the previous state (while skipping self-transitions and recalling preserved data). } @@ -96,17 +96,16 @@ type PluginAfterMethodExecutionContext interface { } // createAndInitializeFlow initializes the Flow and returns a flow Response. -func createAndInitializeFlow(db FlowDB, flow Flow) (*Response, error) { +func createAndInitializeFlow(db FlowDB, flow Flow) (FlowResult, error) { // Wrap the provided FlowDB with additional functionality. dbw := wrapDB(db) // Calculate the expiration time for the Flow. expiresAt := time.Now().Add(flow.TTL).UTC() - // Initialize JSONManagers for stash, payload, and flash data. - - // TODO: Consider implementing types for stash, payload, and flash that extend "jsonmanager.NewJSONManager()". + // TODO: Consider implementing types for stash and payload that extend "jsonmanager.NewJSONManager()". // This could enhance the code structure and provide clearer interfaces for handling these data structures. + // Initialize JSONManagers for stash and payload. stash := jsonmanager.NewJSONManager() payload := jsonmanager.NewJSONManager() @@ -128,29 +127,30 @@ func createAndInitializeFlow(db FlowDB, flow Flow) (*Response, error) { // Generate a response based on the execution result. er := executionResult{nextState: flowModel.CurrentState} - return er.generateResponse(fc) + + return er.generateResponse(fc), nil } // executeFlowMethod processes the Flow and returns a Response. -func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Response, error) { +func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (FlowResult, error) { // Parse the action parameter to get the method name and Flow ID. action, err := utils.ParseActionParam(options.action) if err != nil { - return nil, fmt.Errorf("failed to parse action param: %w", err) + return newFlowResultFromError(flow.ErrorState, ErrorActionParamInvalid.Wrap(err), flow.Debug), nil } // Retrieve the Flow model from the database using the Flow ID. flowModel, err := db.GetFlow(action.FlowID) if err != nil { - if err == sql.ErrNoRows { - return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + if errors.Is(err, sql.ErrNoRows) { + return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil } return nil, fmt.Errorf("failed to get Flow: %w", err) } // Check if the Flow has expired. if time.Now().After(flowModel.ExpiresAt) { - return &Response{State: flow.ErrorState, Error: FlowExpiredError}, nil + return newFlowResultFromError(flow.ErrorState, ErrorFlowExpired, flow.Debug), nil } // Parse stash data from the Flow model. @@ -174,7 +174,8 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Get the available transitions for the current state. transitions := fc.getCurrentTransitions() if transitions == nil { - return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + err2 := errors.New("the state does not allow to continue with the flow") + return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err2), flow.Debug), nil } // Parse raw input data into JSONManager. @@ -189,7 +190,7 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Get the method associated with the action method name. method, err := transitions.getMethod(methodName) if err != nil { - return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil } // Initialize the schema and method context for method execution. @@ -199,7 +200,7 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Check if the method is suspended. if mic.isSuspended { - return &Response{State: flow.ErrorState, Error: OperationNotPermittedError}, nil + return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted, flow.Debug), nil } // Create a methodExecutionContext instance for method execution. @@ -222,5 +223,6 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (*Res // Generate a response based on the execution result. er := *mec.methodResult - return er.generateResponse(fc) + + return er.generateResponse(fc), nil } diff --git a/backend/flowpilot/context_methodExecution.go b/backend/flowpilot/context_methodExecution.go index a2f81e968..661895529 100644 --- a/backend/flowpilot/context_methodExecution.go +++ b/backend/flowpilot/context_methodExecution.go @@ -48,8 +48,8 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio mec.flowModel = *flowModel - // Get inputDataToPersist from the executed method's schema for recording. - inputDataToPersist := mec.input.getDataToPersist() + // Get the data to persists from the executed method's schema for recording. + inputDataToPersist := mec.input.getDataToPersist().String() // Prepare parameters for creating a new Transition in the database. transitionCreationParam := transitionCreationParam{ @@ -57,8 +57,8 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio methodName: mec.methodName, fromState: currentState, toState: executionResult.nextState, - inputData: inputDataToPersist.String(), - errType: executionResult.errType, + inputData: inputDataToPersist, + flowError: executionResult.flowError, } // Create a new Transition in the database. @@ -71,7 +71,7 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio } // continueFlow continues the Flow execution to the specified nextState with an optional error type. -func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, errType *ErrorType) error { +func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { // Check if the specified nextState is valid. if exists := mec.flow.stateExists(nextState); !exists { return errors.New("the execution result contains an invalid state") @@ -80,7 +80,7 @@ func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, errT // Prepare an executionResult for continuing the Flow. methodResult := executionResult{ nextState: nextState, - errType: errType, + flowError: flowError, methodExecutionResult: &methodExecutionResult{ methodName: mec.methodName, schema: mec.input, @@ -131,6 +131,6 @@ func (mec *defaultMethodExecutionContext) ContinueFlow(nextState StateName) erro } // ContinueFlowWithError continues the Flow execution to the specified nextState with an error type. -func (mec *defaultMethodExecutionContext) ContinueFlowWithError(nextState StateName, errType *ErrorType) error { - return mec.continueFlow(nextState, errType) +func (mec *defaultMethodExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { + return mec.continueFlow(nextState, flowErr) } diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go index b5af87c7a..24be19e09 100644 --- a/backend/flowpilot/db.go +++ b/backend/flowpilot/db.go @@ -141,7 +141,7 @@ type transitionCreationParam struct { fromState StateName // Source state of the Transition. toState StateName // Target state of the Transition. inputData string // Input data associated with the Transition. - errType *ErrorType // Optional error type associated with the Transition. + flowError FlowError // Optional flowError associated with the Transition. } // CreateTransitionWithParam creates a new Transition with the given parameters. @@ -165,8 +165,9 @@ func (w *DefaultFlowDBWrapper) CreateTransitionWithParam(p transitionCreationPar } // Set the error code if provided. - if p.errType != nil { - tm.ErrorCode = &p.errType.Code + if p.flowError != nil { + code := p.flowError.Code() + tm.ErrorCode = &code } // Create the Transition in the database. diff --git a/backend/flowpilot/errors.go b/backend/flowpilot/errors.go index 9ba112f75..c018bea0d 100644 --- a/backend/flowpilot/errors.go +++ b/backend/flowpilot/errors.go @@ -1,23 +1,163 @@ package flowpilot -// TODO: Guess it would be nice to add an error interface +import ( + "fmt" + "net/http" +) + +// flowpilotError defines the interface for custom error types in the Flowpilot package. +type flowpilotError interface { + error + + Unwrap() error + Code() string + Message() string + + toPublicError(debug bool) PublicError +} + +// FlowError is an interface representing flow-related errors. +type FlowError interface { + flowpilotError + + Wrap(error) FlowError + Status() int +} + +// InputError is an interface representing input-related errors. +type InputError interface { + flowpilotError + + Wrap(error) InputError +} + +// defaultError is a base struct for custom error types. +type defaultError struct { + origin error // The error origin. + code string // Unique error code. + message string // Contains a description of the error. + errorText string // The string representation of the error. +} -// ErrorType represents a custom error type with a code and message. -type ErrorType struct { - Code string `json:"code"` // Unique error code. - Message string `json:"message"` // Description of the error. +// Code returns the error code. +func (e *defaultError) Code() string { + return e.code } -// Predefined error types +// Message returns the error message. +func (e *defaultError) Message() string { + return e.message +} + +// Unwrap returns the wrapped error. +func (e *defaultError) Unwrap() error { + return e.origin +} + +// Error returns the formatted error message. +func (e *defaultError) Error() string { + return e.errorText +} + +// toPublicError converts the error to a PublicError for public exposure. +func (e *defaultError) toPublicError(debug bool) PublicError { + pe := PublicError{ + Code: e.Code(), + Message: e.Message(), + } + + if debug && e.origin != nil { + str := e.origin.Error() + pe.Origin = &str + } + + return pe +} + +// defaultFlowError is a struct for flow-related errors. +type defaultFlowError struct { + defaultError + + status int // The suggested HTTP status code. +} + +func createErrorText(code, message string, origin error) string { + txt := fmt.Sprintf("%s - %s", code, message) + if origin != nil { + txt = fmt.Sprintf("%s: %s", txt, origin.Error()) + } + return txt +} + +// NewFlowError creates a new FlowError instance. +func NewFlowError(code, message string, status int) FlowError { + return newFlowErrorWithOrigin(code, message, status, nil) +} + +// newFlowErrorWithOrigin creates a new FlowError instance with an origin error. +func newFlowErrorWithOrigin(code, message string, status int, origin error) FlowError { + e := defaultError{ + origin: origin, + code: code, + message: message, + errorText: createErrorText(code, message, origin), + } + + return &defaultFlowError{defaultError: e, status: status} +} + +// Status returns the suggested HTTP status code. +func (e *defaultFlowError) Status() int { + return e.status +} + +// Wrap wraps the error with another error. +func (e *defaultFlowError) Wrap(err error) FlowError { + return newFlowErrorWithOrigin(e.code, e.message, e.status, err) +} + +// defaultInputError is a struct for input-related errors. +type defaultInputError struct { + defaultError +} + +// NewInputError creates a new InputError instance. +func NewInputError(code, message string) InputError { + return newInputErrorWithOrigin(code, message, nil) +} + +// newInputErrorWithOrigin creates a new InputError instance with an origin error. +func newInputErrorWithOrigin(code, message string, origin error) InputError { + e := defaultError{ + origin: origin, + code: code, + message: message, + errorText: createErrorText(code, message, origin), + } + + return &defaultInputError{defaultError: e} +} + +// Wrap wraps the error with another error. +func (e *defaultInputError) Wrap(err error) InputError { + return newInputErrorWithOrigin(e.code, e.message, err) +} + +// Predefined flow error types +var ( + ErrorTechnical = NewFlowError("technical_error", "Something went wrong.", http.StatusInternalServerError) + ErrorFlowExpired = NewFlowError("flow_expired_error", "The flow has expired.", http.StatusGone) + ErrorFlowDiscontinuity = NewFlowError("flow_discontinuity_error", "The flow can't be continued.", http.StatusInternalServerError) + ErrorOperationNotPermitted = NewFlowError("operation_not_permitted_error", "The operation is not permitted.", http.StatusForbidden) + ErrorFormDataInvalid = NewFlowError("form_data_invalid_error", "Form data invalid.", http.StatusBadRequest) + ErrorActionParamInvalid = NewFlowError("action_param_invalid_error", "Action parameter is invalid.", http.StatusBadRequest) +) + +// Predefined input error types var ( - TechnicalError = &ErrorType{Code: "technical_error", Message: "Something went wrong."} - FlowExpiredError = &ErrorType{Code: "flow_expired_error", Message: "The flow has expired."} - FlowDiscontinuityError = &ErrorType{Code: "flow_discontinuity_error", Message: "Thr flow can't be continued."} - OperationNotPermittedError = &ErrorType{Code: "operation_not_permitted_error", Message: "The operation is not permitted."} - FormDataInvalidError = &ErrorType{Code: "form_data_invalid_error", Message: "Form data invalid."} - EmailInvalidError = &ErrorType{Code: "email_invalid_error", Message: "The email address is invalid."} - ValueMissingError = &ErrorType{Code: "value_missing_error", Message: "Missing value."} - ValueInvalidError = &ErrorType{Code: "value_invalid_error", Message: "The value is invalid."} - ValueTooLongError = &ErrorType{Code: "value_too_long_error", Message: "Value is too long."} - ValueTooShortError = &ErrorType{Code: "value_too_short_error", Message: "Value is too short."} + ErrorEmailInvalid = NewInputError("email_invalid_error", "The email address is invalid.") + ErrorValueMissing = NewInputError("value_missing_error", "Missing value.") + ErrorValueInvalid = NewInputError("value_invalid_error", "The value is invalid.") + ErrorValueTooLong = NewInputError("value_too_long_error", "Value is too long.") + ErrorValueTooShort = NewInputError("value_too_short_error", "Value is too short.") ) diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index 1e929d71d..b6db8b151 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -40,8 +40,6 @@ func WithInputData(inputData InputData) func(*flowExecutionOptions) { } } -// TODO: Not sure if we really want to have these types for state and method names - // StateName represents the name of a state in a Flow. type StateName string @@ -88,6 +86,7 @@ type Flow struct { ErrorState StateName // State representing errors. EndState StateName // Final state of the flow. TTL time.Duration // Time-to-live for the flow. + Debug bool // Enables debug mode. } // stateExists checks if a state exists in the Flow. @@ -142,7 +141,7 @@ func (f *Flow) validate() error { } // Execute handles the execution of actions for a Flow. -func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (*Response, error) { +func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) { // Process execution options. var executionOptions flowExecutionOptions for _, option := range opts { @@ -166,10 +165,15 @@ func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (*Respons return executeFlowMethod(db, *f, executionOptions) } -// ErrorResponse returns an error response for the Flow. -func (f *Flow) ErrorResponse() *Response { - return &Response{ - State: f.ErrorState, - Error: TechnicalError, +// ResultFromError returns an error response for the Flow. +func (f *Flow) ResultFromError(err error) (result FlowResult) { + flowError := ErrorTechnical + + if err2, ok := err.(FlowError); ok { + flowError = err2 + } else { + flowError = flowError.Wrap(err) } + + return newFlowResultFromError(f.ErrorState, flowError, f.Debug) } diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go index 7c3640fce..97344e7e6 100644 --- a/backend/flowpilot/input.go +++ b/backend/flowpilot/input.go @@ -29,7 +29,7 @@ type Input interface { CompareWithStash(b bool) Input setValue(value interface{}) Input - setError(errType *ErrorType) + setError(inputError InputError) getName() string shouldPersist() bool shouldPreserve() bool @@ -55,23 +55,11 @@ type DefaultInput struct { maxLength *int required *bool hidden *bool - errorType *ErrorType + error InputError defaultExtraInputOptions } -// PublicInput represents an input field for public exposure. -type PublicInput struct { - Name string `json:"name"` - Type InputType `json:"type"` - Value interface{} `json:"value,omitempty"` - MinLength *int `json:"min_length,omitempty"` - MaxLength *int `json:"max_length,omitempty"` - Required *bool `json:"required,omitempty"` - Hidden *bool `json:"hidden,omitempty"` - Error *ErrorType `json:"error,omitempty"` -} - // newInput creates a new DefaultInput instance with provided parameters. func newInput(name string, t InputType, persistValue bool) Input { return &DefaultInput{ @@ -187,8 +175,8 @@ func (i *DefaultInput) getName() string { } // setError sets an error to the given input field. -func (i *DefaultInput) setError(errType *ErrorType) { - i.errorType = errType +func (i *DefaultInput) setError(inputError InputError) { + i.error = inputError } // shouldPersist indicates the value should be persisted. @@ -222,12 +210,12 @@ func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadO } if i.required != nil && *i.required && (inputValue == nil || len(*inputValue) <= 0) { - i.errorType = ValueMissingError + i.error = ErrorValueMissing return false } if i.compareWithStash && inputValue != nil && stashValue != nil && *inputValue != *stashValue { - i.errorType = ValueInvalidError + i.error = ErrorValueInvalid return false } @@ -238,14 +226,14 @@ func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadO if i.minLength != nil { if len(*inputValue) < *i.minLength { - i.errorType = ValueTooShortError + i.error = ErrorValueTooShort return false } } if i.maxLength != nil { if len(*inputValue) > *i.maxLength { - i.errorType = ValueTooLongError + i.error = ErrorValueTooLong return false } } @@ -253,7 +241,7 @@ func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadO if i.dataType == EmailType { pattern := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) if matched := pattern.MatchString(*inputValue); !matched { - i.errorType = EmailInvalidError + i.error = ErrorEmailInvalid return false } } @@ -263,6 +251,13 @@ func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadO // toPublicInput converts the DefaultInput to a PublicInput for public exposure. func (i *DefaultInput) toPublicInput() *PublicInput { + var pe *PublicError + + if i.error != nil { + e := i.error.toPublicError(true) + pe = &e + } + return &PublicInput{ Name: i.name, Type: i.dataType, @@ -271,6 +266,6 @@ func (i *DefaultInput) toPublicInput() *PublicInput { MaxLength: i.maxLength, Required: i.required, Hidden: i.hidden, - Error: i.errorType, + Error: pe, } } diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index 2ee6ed7aa..4c449e627 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -14,7 +14,7 @@ type InitializationSchema interface { type MethodExecutionSchema interface { Get(path string) gjson.Result Set(path string, value interface{}) error - SetError(inputName string, errType *ErrorType) + SetError(inputName string, inputError InputError) getInput(name string) Input getOutputData() jsonmanager.ReadOnlyJSONManager @@ -98,10 +98,10 @@ func (s *defaultSchema) getInput(name string) Input { return nil } -// SetError sets an error type for an input field in the schema. -func (s *defaultSchema) SetError(inputName string, errType *ErrorType) { +// SetError sets an error for an input field in the schema. +func (s *defaultSchema) SetError(inputName string, inputError InputError) { if i := s.getInput(inputName); i != nil { - i.setError(errType) + i.setError(inputError) } } diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index 32b217523..348aeb7e7 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -3,6 +3,7 @@ package flowpilot import ( "fmt" "github.com/teamhanko/hanko/backend/flowpilot/utils" + "net/http" ) // Link represents a link to an action. @@ -21,12 +22,69 @@ func (ls *Links) Add(l Link) { *ls = append(*ls, l) } -// Response represents the response of an action execution. -type Response struct { - State StateName `json:"state"` - Payload interface{} `json:"payload,omitempty"` - Links Links `json:"links"` - Error *ErrorType `json:"error,omitempty"` +// PublicError represents an error for public exposure. +type PublicError struct { + Code string `json:"code"` + Message string `json:"message"` + Origin *string `json:"origin,omitempty"` +} + +// PublicInput represents an input field for public exposure. +type PublicInput struct { + Name string `json:"name"` + Type InputType `json:"type"` + Value interface{} `json:"value,omitempty"` + MinLength *int `json:"min_length,omitempty"` + MaxLength *int `json:"max_length,omitempty"` + Required *bool `json:"required,omitempty"` + Hidden *bool `json:"hidden,omitempty"` + Error *PublicError `json:"error,omitempty"` +} + +// PublicResponse represents the response of an action execution. +type PublicResponse struct { + State StateName `json:"state"` + Status int `json:"status"` + Payload interface{} `json:"payload,omitempty"` + Links Links `json:"links"` + Error *PublicError `json:"error,omitempty"` +} + +// FlowResult interface defines methods for obtaining response and status. +type FlowResult interface { + Response() PublicResponse + Status() int +} + +// DefaultFlowResult implements FlowResult interface. +type DefaultFlowResult struct { + PublicResponse +} + +// newFlowResultFromResponse creates a FlowResult from a PublicResponse. +func newFlowResultFromResponse(response PublicResponse) FlowResult { + return DefaultFlowResult{PublicResponse: response} +} + +// newFlowResultFromError creates a FlowResult from a FlowError. +func newFlowResultFromError(stateName StateName, flowError FlowError, debug bool) FlowResult { + pe := flowError.toPublicError(debug) + + return DefaultFlowResult{PublicResponse: PublicResponse{ + State: stateName, + Status: flowError.Status(), + Error: &pe, + }} +} + +// Response returns the PublicResponse. +func (r DefaultFlowResult) Response() PublicResponse { + return r.PublicResponse +} + +// Status returns the HTTP status code. +func (r DefaultFlowResult) Status() int { + return r.PublicResponse.Status } // methodExecutionResult holds the result of a method execution. @@ -38,24 +96,34 @@ type methodExecutionResult struct { // executionResult holds the result of an action execution. type executionResult struct { nextState StateName - errType *ErrorType + flowError FlowError *methodExecutionResult } // generateResponse generates a response based on the execution result. -func (er *executionResult) generateResponse(fc defaultFlowContext) (*Response, error) { +func (er *executionResult) generateResponse(fc defaultFlowContext) FlowResult { // Generate links for the response. links := er.generateLinks(fc) // Create the response object. - resp := &Response{ + resp := PublicResponse{ State: er.nextState, + Status: http.StatusOK, Payload: fc.payload.Unmarshal(), Links: links, - Error: er.errType, } - return resp, nil + + // Include flow error if present. + if er.flowError != nil { + status := er.flowError.Status() + publicError := er.flowError.toPublicError(false) + + resp.Status = status + resp.Error = &publicError + } + + return newFlowResultFromResponse(resp) } // generateLinks generates a collection of links based on the execution result. From 6add26431c848f43899c4580af629c6646deaca3 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Tue, 5 Sep 2023 16:29:06 +0200 Subject: [PATCH 009/278] chore: use the wording 'action' instead of 'method' or 'link' --- .../flow_api_test/{methods.go => actions.go} | 50 +++++----- .../flow_api_test/static/generic_client.html | 24 ++--- backend/flow_api_test/types.go | 24 ++--- backend/flowpilot/builder.go | 4 +- backend/flowpilot/context.go | 94 +++++++++---------- ...hodExecution.go => context_action_exec.go} | 44 ++++----- ...itialization.go => context_action_init.go} | 14 +-- backend/flowpilot/context_flow.go | 4 +- backend/flowpilot/db.go | 10 +- backend/flowpilot/flow.go | 38 ++++---- backend/flowpilot/input_schema.go | 18 ++-- backend/flowpilot/response.go | 86 ++++++++--------- backend/flowpilot/utils/param.go | 22 ++--- .../20230810173315_create_flows.up.fizz | 4 +- backend/persistence/models/flowdb.go | 6 +- backend/persistence/models/transition.go | 4 +- 16 files changed, 223 insertions(+), 223 deletions(-) rename backend/flow_api_test/{methods.go => actions.go} (88%) rename backend/flowpilot/{context_methodExecution.go => context_action_exec.go} (72%) rename backend/flowpilot/{context_methodInitialization.go => context_action_init.go} (57%) diff --git a/backend/flow_api_test/methods.go b/backend/flow_api_test/actions.go similarity index 88% rename from backend/flow_api_test/methods.go rename to backend/flow_api_test/actions.go index 5bba7cb42..2b5b529a9 100644 --- a/backend/flow_api_test/methods.go +++ b/backend/flow_api_test/actions.go @@ -8,8 +8,8 @@ import ( type SubmitEmail struct{} -func (m SubmitEmail) GetName() flowpilot.MethodName { - return MethodSubmitEmail +func (m SubmitEmail) GetName() flowpilot.ActionName { + return ActionSubmitEmail } func (m SubmitEmail) GetDescription() string { @@ -47,8 +47,8 @@ func (m SubmitEmail) Execute(c flowpilot.ExecutionContext) error { type GetWAChallenge struct{} -func (m GetWAChallenge) GetName() flowpilot.MethodName { - return MethodGetWAChallenge +func (m GetWAChallenge) GetName() flowpilot.ActionName { + return ActionGetWAChallenge } func (m GetWAChallenge) GetDescription() string { @@ -64,8 +64,8 @@ func (m GetWAChallenge) Execute(c flowpilot.ExecutionContext) error { type VerifyWAPublicKey struct{} -func (m VerifyWAPublicKey) GetName() flowpilot.MethodName { - return MethodVerifyWAPublicKey +func (m VerifyWAPublicKey) GetName() flowpilot.ActionName { + return ActionVerifyWAPublicKey } func (m VerifyWAPublicKey) GetDescription() string { @@ -86,8 +86,8 @@ func (m VerifyWAPublicKey) Execute(c flowpilot.ExecutionContext) error { type SubmitExistingPassword struct{} -func (m SubmitExistingPassword) GetName() flowpilot.MethodName { - return MethodSubmitExistingPassword +func (m SubmitExistingPassword) GetName() flowpilot.ActionName { + return ActionSubmitExistingPassword } func (m SubmitExistingPassword) GetDescription() string { @@ -126,8 +126,8 @@ func (m SubmitExistingPassword) Execute(c flowpilot.ExecutionContext) error { type RequestRecovery struct{} -func (m RequestRecovery) GetName() flowpilot.MethodName { - return MethodRequestRecovery +func (m RequestRecovery) GetName() flowpilot.ActionName { + return ActionRequestRecovery } func (m RequestRecovery) GetDescription() string { @@ -136,7 +136,7 @@ func (m RequestRecovery) GetDescription() string { func (m RequestRecovery) Initialize(c flowpilot.InitializationContext) { if myFlowConfig.isEnabled(FlowOptionSecondFactorFlow) { - c.SuspendMethod() + c.SuspendAction() } } @@ -147,8 +147,8 @@ func (m RequestRecovery) Execute(c flowpilot.ExecutionContext) error { type SubmitPasscodeCode struct{} -func (m SubmitPasscodeCode) GetName() flowpilot.MethodName { - return MethodSubmitPasscodeCode +func (m SubmitPasscodeCode) GetName() flowpilot.ActionName { + return ActionSubmitPasscodeCode } func (m SubmitPasscodeCode) GetDescription() string { @@ -187,8 +187,8 @@ func (m SubmitPasscodeCode) Execute(c flowpilot.ExecutionContext) error { type CreateUser struct{} -func (m CreateUser) GetName() flowpilot.MethodName { - return MethodCreateUser +func (m CreateUser) GetName() flowpilot.ActionName { + return ActionCreateUser } func (m CreateUser) GetDescription() string { @@ -214,8 +214,8 @@ func (m CreateUser) Execute(c flowpilot.ExecutionContext) error { type SubmitNewPassword struct{} -func (m SubmitNewPassword) GetName() flowpilot.MethodName { - return MethodSubmitNewPassword +func (m SubmitNewPassword) GetName() flowpilot.ActionName { + return ActionSubmitNewPassword } func (m SubmitNewPassword) GetDescription() string { @@ -246,8 +246,8 @@ func (m SubmitNewPassword) Execute(c flowpilot.ExecutionContext) error { type GetWAAssertion struct{} -func (m GetWAAssertion) GetName() flowpilot.MethodName { - return MethodGetWAAssertion +func (m GetWAAssertion) GetName() flowpilot.ActionName { + return ActionGetWAAssertion } func (m GetWAAssertion) GetDescription() string { @@ -263,8 +263,8 @@ func (m GetWAAssertion) Execute(c flowpilot.ExecutionContext) error { type VerifyWAAssertion struct{} -func (m VerifyWAAssertion) GetName() flowpilot.MethodName { - return MethodVerifyWAAssertion +func (m VerifyWAAssertion) GetName() flowpilot.ActionName { + return ActionVerifyWAAssertion } func (m VerifyWAAssertion) GetDescription() string { @@ -285,8 +285,8 @@ func (m VerifyWAAssertion) Execute(c flowpilot.ExecutionContext) error { type SkipPasskeyCreation struct{} -func (m SkipPasskeyCreation) GetName() flowpilot.MethodName { - return MethodSkipPasskeyCreation +func (m SkipPasskeyCreation) GetName() flowpilot.ActionName { + return ActionSkipPasskeyCreation } func (m SkipPasskeyCreation) GetDescription() string { @@ -301,8 +301,8 @@ func (m SkipPasskeyCreation) Execute(c flowpilot.ExecutionContext) error { type Back struct{} -func (m Back) GetName() flowpilot.MethodName { - return MethodBack +func (m Back) GetName() flowpilot.ActionName { + return ActionBack } func (m Back) GetDescription() string { diff --git a/backend/flow_api_test/static/generic_client.html b/backend/flow_api_test/static/generic_client.html index 3f33a75d5..5ab26906e 100644 --- a/backend/flow_api_test/static/generic_client.html +++ b/backend/flow_api_test/static/generic_client.html @@ -108,19 +108,19 @@

✨ Login

container.appendChild(executeHandlerButton); - const methodsHeadline = document.createElement('h3'); - methodsHeadline.textContent = "🕹 Methods"; - container.appendChild(methodsHeadline); + const actionsHeadline = document.createElement('h3'); + actionsHeadline.textContent = "🕹 Actions"; + container.appendChild(actionsHeadline); - if (data.links) { - data.links.forEach(link => { + if (data.actions) { + data.actions.forEach(action => { const form = document.createElement('form'); - form.action = link.href; + form.action = action.href; form.method = 'POST'; - const methodHeadline = document.createElement('h4'); - methodHeadline.textContent = "⚡ Method: " + link.method_name; - form.appendChild(methodHeadline); + const actionHeadline = document.createElement('h4'); + actionHeadline.textContent = "⚡ Action: " + action.action_name; + form.appendChild(actionHeadline); const descriptionHeadline = document.createElement('h5'); @@ -128,17 +128,17 @@

✨ Login

form.appendChild(descriptionHeadline); const description = document.createElement('div'); - description.textContent = link.description; + description.textContent = action.description; form.appendChild(description); const inputsHeadline = document.createElement('h5'); inputsHeadline.textContent = "⛳ Inputs"; form.appendChild(inputsHeadline); - if (link.inputs) { + if (action.inputs) { const inputList = document.createElement('ul'); - link.inputs.forEach(input => { + action.inputs.forEach(input => { const inputListItem = document.createElement('li'); const label = document.createElement("label"); diff --git a/backend/flow_api_test/types.go b/backend/flow_api_test/types.go index 526fac812..f31818584 100644 --- a/backend/flow_api_test/types.go +++ b/backend/flow_api_test/types.go @@ -20,16 +20,16 @@ const ( ) const ( - MethodSubmitEmail flowpilot.MethodName = "submit_email" - MethodGetWAChallenge flowpilot.MethodName = "get_webauthn_challenge" - MethodVerifyWAPublicKey flowpilot.MethodName = "verify_webauthn_public_key" - MethodGetWAAssertion flowpilot.MethodName = "get_webauthn_assertion" - MethodVerifyWAAssertion flowpilot.MethodName = "verify_webauthn_assertion_response" - MethodSubmitExistingPassword flowpilot.MethodName = "submit_existing_password" - MethodSubmitNewPassword flowpilot.MethodName = "submit_new_password" - MethodRequestRecovery flowpilot.MethodName = "request_recovery" - MethodSubmitPasscodeCode flowpilot.MethodName = "submit_passcode_code" - MethodCreateUser flowpilot.MethodName = "create_user" - MethodSkipPasskeyCreation flowpilot.MethodName = "skip_passkey_creation" - MethodBack flowpilot.MethodName = "back" + ActionSubmitEmail flowpilot.ActionName = "submit_email" + ActionGetWAChallenge flowpilot.ActionName = "get_webauthn_challenge" + ActionVerifyWAPublicKey flowpilot.ActionName = "verify_webauthn_public_key" + ActionGetWAAssertion flowpilot.ActionName = "get_webauthn_assertion" + ActionVerifyWAAssertion flowpilot.ActionName = "verify_webauthn_assertion_response" + ActionSubmitExistingPassword flowpilot.ActionName = "submit_existing_password" + ActionSubmitNewPassword flowpilot.ActionName = "submit_new_password" + ActionRequestRecovery flowpilot.ActionName = "request_recovery" + ActionSubmitPasscodeCode flowpilot.ActionName = "submit_passcode_code" + ActionCreateUser flowpilot.ActionName = "create_user" + ActionSkipPasskeyCreation flowpilot.ActionName = "skip_passkey_creation" + ActionBack flowpilot.ActionName = "back" ) diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 980ac1406..5bdc1b0b9 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -30,10 +30,10 @@ func (fb *FlowBuilder) TTL(ttl time.Duration) *FlowBuilder { } // State adds a new state transition to the FlowBuilder. -func (fb *FlowBuilder) State(state StateName, mList ...Method) *FlowBuilder { +func (fb *FlowBuilder) State(state StateName, mList ...Action) *FlowBuilder { var transitions Transitions for _, m := range mList { - transitions = append(transitions, Transition{Method: m}) + transitions = append(transitions, Transition{Action: m}) } fb.flow[state] = transitions return fb diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 494ea1366..5adc6cdfd 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -36,35 +36,35 @@ type flowContext interface { StateExists(stateName StateName) bool } -// methodInitializationContext represents the basic context for a Flow method's initialization. -type methodInitializationContext interface { +// actionInitializationContext represents the basic context for a Flow action's initialization. +type actionInitializationContext interface { // AddInputs adds input parameters to the schema. AddInputs(inputList ...Input) // Stash returns the ReadOnlyJSONManager for accessing stash data. Stash() jsonmanager.ReadOnlyJSONManager - // SuspendMethod suspends the current method's execution. - SuspendMethod() + // SuspendAction suspends the current action's execution. + SuspendAction() } -// methodExecutionContext represents the context for a method execution. -type methodExecutionContext interface { +// actionExecutionContext represents the context for a action execution. +type actionExecutionContext interface { flowContext - // Input returns the MethodExecutionSchema for the method. - Input() MethodExecutionSchema + // Input returns the ExecutionSchema for the action. + Input() ExecutionSchema - // TODO: FetchMethodInput (for a method name) is maybe useless and can be removed or replaced. + // TODO: FetchActionInput (for a action name) is maybe useless and can be removed or replaced. - // FetchMethodInput fetches input data for a specific method. - FetchMethodInput(methodName MethodName) (jsonmanager.ReadOnlyJSONManager, error) + // FetchActionInput fetches input data for a specific action. + FetchActionInput(actionName ActionName) (jsonmanager.ReadOnlyJSONManager, error) // ValidateInputData validates the input data against the schema. ValidateInputData() bool // CopyInputValuesToStash copies specified inputs to the stash. CopyInputValuesToStash(inputNames ...string) error } -// methodExecutionContinuationContext represents the context within a method continuation. -type methodExecutionContinuationContext interface { - methodExecutionContext +// actionExecutionContinuationContext represents the context within an action continuation. +type actionExecutionContinuationContext interface { + actionExecutionContext // ContinueFlow continues the Flow execution to the specified next state. ContinueFlow(nextState StateName) error // ContinueFlowWithError continues the Flow execution to the specified next state with an error. @@ -73,26 +73,26 @@ type methodExecutionContinuationContext interface { // TODO: Implement a function to step back to the previous state (while skipping self-transitions and recalling preserved data). } -// InitializationContext is a shorthand for methodInitializationContext within flow methods. +// InitializationContext is a shorthand for actionInitializationContext within flow actions. type InitializationContext interface { - methodInitializationContext + actionInitializationContext } -// ExecutionContext is a shorthand for methodExecutionContinuationContext within flow methods. +// ExecutionContext is a shorthand for actionExecutionContinuationContext within flow actions. type ExecutionContext interface { - methodExecutionContinuationContext + actionExecutionContinuationContext } // TODO: The following interfaces are meant for a plugin system. #tbd -// PluginBeforeMethodExecutionContext represents the context for a plugin before a method execution. -type PluginBeforeMethodExecutionContext interface { - methodExecutionContinuationContext +// PluginBeforeActionExecutionContext represents the context for a plugin before an action execution. +type PluginBeforeActionExecutionContext interface { + actionExecutionContinuationContext } -// PluginAfterMethodExecutionContext represents the context for a plugin after a method execution. -type PluginAfterMethodExecutionContext interface { - methodExecutionContext +// PluginAfterActionExecutionContext represents the context for a plugin after an action execution. +type PluginAfterActionExecutionContext interface { + actionExecutionContext } // createAndInitializeFlow initializes the Flow and returns a flow Response. @@ -131,16 +131,16 @@ func createAndInitializeFlow(db FlowDB, flow Flow) (FlowResult, error) { return er.generateResponse(fc), nil } -// executeFlowMethod processes the Flow and returns a Response. -func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (FlowResult, error) { - // Parse the action parameter to get the method name and Flow ID. - action, err := utils.ParseActionParam(options.action) +// executeFlowAction processes the Flow and returns a Response. +func executeFlowAction(db FlowDB, flow Flow, options flowExecutionOptions) (FlowResult, error) { + // Parse the actionParam parameter to get the actionParam name and Flow ID. + actionParam, err := utils.ParseActionParam(options.action) if err != nil { return newFlowResultFromError(flow.ErrorState, ErrorActionParamInvalid.Wrap(err), flow.Debug), nil } // Retrieve the Flow model from the database using the Flow ID. - flowModel, err := db.GetFlow(action.FlowID) + flowModel, err := db.GetFlow(actionParam.FlowID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil @@ -185,44 +185,44 @@ func executeFlowMethod(db FlowDB, flow Flow, options flowExecutionOptions) (Flow return nil, fmt.Errorf("failed to parse input data: %w", err) } - // Create a MethodName from the parsed action method name. - methodName := MethodName(action.MethodName) - // Get the method associated with the action method name. - method, err := transitions.getMethod(methodName) + // Create a ActionName from the parsed actionParam name. + actionName := ActionName(actionParam.ActionName) + // Get the action associated with the actionParam name. + action, err := transitions.getAction(actionName) if err != nil { return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil } - // Initialize the schema and method context for method execution. + // Initialize the schema and action context for action execution. schema := newSchemaWithInputData(inputJSON) - mic := defaultMethodInitializationContext{schema: schema.toInitializationSchema(), stash: stash} - method.Initialize(&mic) + mic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: stash} + action.Initialize(&mic) - // Check if the method is suspended. + // Check if the action is suspended. if mic.isSuspended { return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted, flow.Debug), nil } - // Create a methodExecutionContext instance for method execution. - mec := defaultMethodExecutionContext{ - methodName: methodName, + // Create a actionExecutionContext instance for action execution. + mec := defaultActionExecutionContext{ + actionName: actionName, input: schema, defaultFlowContext: fc, } - // Execute the method and handle any errors. - err = method.Execute(&mec) + // Execute the action and handle any errors. + err = action.Execute(&mec) if err != nil { - return nil, fmt.Errorf("the method failed to handle the request: %w", err) + return nil, fmt.Errorf("the action failed to handle the request: %w", err) } - // Ensure that the method has set a result object. - if mec.methodResult == nil { - return nil, errors.New("the method has not set a result object") + // Ensure that the action has set a result object. + if mec.executionResult == nil { + return nil, errors.New("the action has not set a result object") } // Generate a response based on the execution result. - er := *mec.methodResult + er := *mec.executionResult return er.generateResponse(fc), nil } diff --git a/backend/flowpilot/context_methodExecution.go b/backend/flowpilot/context_action_exec.go similarity index 72% rename from backend/flowpilot/context_methodExecution.go rename to backend/flowpilot/context_action_exec.go index 661895529..9cd4c3865 100644 --- a/backend/flowpilot/context_methodExecution.go +++ b/backend/flowpilot/context_action_exec.go @@ -6,17 +6,17 @@ import ( "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" ) -// defaultMethodExecutionContext is the default implementation of the methodExecutionContext interface. -type defaultMethodExecutionContext struct { - methodName MethodName // Name of the method being executed. - input MethodExecutionSchema // JSONManager for accessing input data. - methodResult *executionResult // Result of the method execution. +// defaultActionExecutionContext is the default implementation of the actionExecutionContext interface. +type defaultActionExecutionContext struct { + actionName ActionName // Name of the action being executed. + input ExecutionSchema // JSONManager for accessing input data. + executionResult *executionResult // Result of the action execution. defaultFlowContext // Embedding the defaultFlowContext for common context fields. } -// saveNextState updates the Flow's state and saves Transition data after method execution. -func (mec *defaultMethodExecutionContext) saveNextState(executionResult executionResult) error { +// saveNextState updates the Flow's state and saves Transition data after action execution. +func (mec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { currentState := mec.flowModel.CurrentState previousState := mec.flowModel.PreviousState @@ -48,13 +48,13 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio mec.flowModel = *flowModel - // Get the data to persists from the executed method's schema for recording. + // Get the data to persists from the executed action schema for recording. inputDataToPersist := mec.input.getDataToPersist().String() // Prepare parameters for creating a new Transition in the database. transitionCreationParam := transitionCreationParam{ flowID: mec.flowModel.ID, - methodName: mec.methodName, + actionName: mec.actionName, fromState: currentState, toState: executionResult.nextState, inputData: inputDataToPersist, @@ -71,45 +71,45 @@ func (mec *defaultMethodExecutionContext) saveNextState(executionResult executio } // continueFlow continues the Flow execution to the specified nextState with an optional error type. -func (mec *defaultMethodExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { +func (mec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { // Check if the specified nextState is valid. if exists := mec.flow.stateExists(nextState); !exists { return errors.New("the execution result contains an invalid state") } // Prepare an executionResult for continuing the Flow. - methodResult := executionResult{ + result := executionResult{ nextState: nextState, flowError: flowError, - methodExecutionResult: &methodExecutionResult{ - methodName: mec.methodName, + actionExecutionResult: &actionExecutionResult{ + actionName: mec.actionName, schema: mec.input, }, } // Save the next state and transition data. - err := mec.saveNextState(methodResult) + err := mec.saveNextState(result) if err != nil { return fmt.Errorf("failed to save the transition data: %w", err) } - mec.methodResult = &methodResult + mec.executionResult = &result return nil } -// Input returns the MethodExecutionSchema for accessing input data. -func (mec *defaultMethodExecutionContext) Input() MethodExecutionSchema { +// Input returns the ExecutionSchema for accessing input data. +func (mec *defaultActionExecutionContext) Input() ExecutionSchema { return mec.input } // Payload returns the JSONManager for accessing payload data. -func (mec *defaultMethodExecutionContext) Payload() jsonmanager.JSONManager { +func (mec *defaultActionExecutionContext) Payload() jsonmanager.JSONManager { return mec.payload } // CopyInputValuesToStash copies specified inputs to the stash. -func (mec *defaultMethodExecutionContext) CopyInputValuesToStash(inputNames ...string) error { +func (mec *defaultActionExecutionContext) CopyInputValuesToStash(inputNames ...string) error { for _, inputName := range inputNames { // Copy input values to the stash. err := mec.stash.Set(inputName, mec.input.Get(inputName).Value()) @@ -121,16 +121,16 @@ func (mec *defaultMethodExecutionContext) CopyInputValuesToStash(inputNames ...s } // ValidateInputData validates the input data against the schema. -func (mec *defaultMethodExecutionContext) ValidateInputData() bool { +func (mec *defaultActionExecutionContext) ValidateInputData() bool { return mec.input.validateInputData(mec.flowModel.CurrentState, mec.stash) } // ContinueFlow continues the Flow execution to the specified nextState. -func (mec *defaultMethodExecutionContext) ContinueFlow(nextState StateName) error { +func (mec *defaultActionExecutionContext) ContinueFlow(nextState StateName) error { return mec.continueFlow(nextState, nil) } // ContinueFlowWithError continues the Flow execution to the specified nextState with an error type. -func (mec *defaultMethodExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { +func (mec *defaultActionExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { return mec.continueFlow(nextState, flowErr) } diff --git a/backend/flowpilot/context_methodInitialization.go b/backend/flowpilot/context_action_init.go similarity index 57% rename from backend/flowpilot/context_methodInitialization.go rename to backend/flowpilot/context_action_init.go index 2da6a8517..d17095c8f 100644 --- a/backend/flowpilot/context_methodInitialization.go +++ b/backend/flowpilot/context_action_init.go @@ -2,24 +2,24 @@ package flowpilot import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" -// defaultMethodInitializationContext is the default implementation of the methodInitializationContext interface. -type defaultMethodInitializationContext struct { - schema InitializationSchema // InitializationSchema for method initialization. +// defaultActionInitializationContext is the default implementation of the actionInitializationContext interface. +type defaultActionInitializationContext struct { + schema InitializationSchema // InitializationSchema for action initialization. isSuspended bool // Flag indicating if the method is suspended. stash jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing stash data. } // AddInputs adds input data to the InitializationSchema and returns a defaultSchema instance. -func (mic *defaultMethodInitializationContext) AddInputs(inputList ...Input) { +func (mic *defaultActionInitializationContext) AddInputs(inputList ...Input) { mic.schema.AddInputs(inputList...) } -// SuspendMethod sets the isSuspended flag to indicate the method is suspended. -func (mic *defaultMethodInitializationContext) SuspendMethod() { +// SuspendAction sets the isSuspended flag to indicate the action is suspended. +func (mic *defaultActionInitializationContext) SuspendAction() { mic.isSuspended = true } // Stash returns the ReadOnlyJSONManager for accessing stash data. -func (mic *defaultMethodInitializationContext) Stash() jsonmanager.ReadOnlyJSONManager { +func (mic *defaultActionInitializationContext) Stash() jsonmanager.ReadOnlyJSONManager { return mic.stash } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index 45bf8bea5..9bab0085e 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -72,9 +72,9 @@ func (fc *defaultFlowContext) StateExists(stateName StateName) bool { } // FetchMethodInput fetches input data for a specific method. -func (fc *defaultFlowContext) FetchMethodInput(methodName MethodName) (jsonmanager.ReadOnlyJSONManager, error) { +func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (jsonmanager.ReadOnlyJSONManager, error) { // Find the last Transition with the specified method from the database wrapper. - t, err := fc.dbw.FindLastTransitionWithMethod(fc.flowModel.ID, methodName) + t, err := fc.dbw.FindLastTransitionWithAction(fc.flowModel.ID, methodName) if err != nil { return nil, fmt.Errorf("failed to get last Transition from dbw: %w", err) } diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go index 24be19e09..dedc88fc8 100644 --- a/backend/flowpilot/db.go +++ b/backend/flowpilot/db.go @@ -23,7 +23,7 @@ type FlowModel struct { type TransitionModel struct { ID uuid.UUID // Unique ID of the Transition. FlowID uuid.UUID // ID of the associated Flow. - Method MethodName // Name of the method associated with the Transition. + Action ActionName // Name of the action associated with the Action. FromState StateName // Source state of the Transition. ToState StateName // Target state of the Transition. InputData string // Input data associated with the Transition. @@ -39,9 +39,9 @@ type FlowDB interface { UpdateFlow(flowModel FlowModel) error CreateTransition(transitionModel TransitionModel) error - // TODO: "FindLastTransitionWithMethod" might be useless, or can be replaced. + // TODO: "FindLastTransitionWithAction" might be useless, or can be replaced. - FindLastTransitionWithMethod(flowID uuid.UUID, method MethodName) (*TransitionModel, error) + FindLastTransitionWithAction(flowID uuid.UUID, method ActionName) (*TransitionModel, error) } // FlowDBWrapper is an extended FlowDB interface that includes additional methods. @@ -137,7 +137,7 @@ func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowMode // transitionCreationParam holds parameters for creating a new Transition. type transitionCreationParam struct { flowID uuid.UUID // ID of the associated Flow. - methodName MethodName // Name of the method associated with the Transition. + actionName ActionName // Name of the action associated with the Transition. fromState StateName // Source state of the Transition. toState StateName // Target state of the Transition. inputData string // Input data associated with the Transition. @@ -156,7 +156,7 @@ func (w *DefaultFlowDBWrapper) CreateTransitionWithParam(p transitionCreationPar tm := TransitionModel{ ID: transitionID, FlowID: p.flowID, - Method: p.methodName, + Action: p.actionName, FromState: p.fromState, ToState: p.toState, InputData: p.inputData, diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index b6db8b151..4c0fa3d4a 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -26,7 +26,7 @@ type flowExecutionOptions struct { inputData InputData } -// WithActionParam sets the MethodName for flowExecutionOptions. +// WithActionParam sets the ActionName for flowExecutionOptions. func WithActionParam(action string) func(*flowExecutionOptions) { return func(f *flowExecutionOptions) { f.action = action @@ -43,36 +43,36 @@ func WithInputData(inputData InputData) func(*flowExecutionOptions) { // StateName represents the name of a state in a Flow. type StateName string -// MethodName represents the name of a method associated with a Transition. -type MethodName string +// ActionName represents the name of a action associated with a Transition. +type ActionName string -// TODO: Should it be possible to partially implement the Method interface? E.g. when a method does not require initialization. +// TODO: Should it be possible to partially implement the Action interface? E.g. when a action does not require initialization. -// Method defines the interface for flow methods. -type Method interface { - GetName() MethodName // Get the method name. - GetDescription() string // Get the method description. - Initialize(InitializationContext) // Initialize the method. - Execute(ExecutionContext) error // Execute the method. +// Action defines the interface for flow actions. +type Action interface { + GetName() ActionName // Get the action name. + GetDescription() string // Get the action description. + Initialize(InitializationContext) // Initialize the action. + Execute(ExecutionContext) error // Execute the action. } -// Transition holds a method associated with a state transition. +// Transition holds an action associated with a state transition. type Transition struct { - Method Method + Action Action } // Transitions is a collection of Transition instances. type Transitions []Transition -// getMethod returns the Method associated with the specified name. -func (ts *Transitions) getMethod(methodName MethodName) (Method, error) { +// getAction returns the Action associated with the specified name. +func (ts *Transitions) getAction(actionName ActionName) (Action, error) { for _, t := range *ts { - if t.Method.GetName() == methodName { - return t.Method, nil + if t.Action.GetName() == actionName { + return t.Action, nil } } - return nil, errors.New(fmt.Sprintf("method '%s' not valid", methodName)) + return nil, errors.New(fmt.Sprintf("action '%s' not valid", actionName)) } // StateTransitions maps states to associated Transitions. @@ -135,7 +135,7 @@ func (f *Flow) validate() error { return fmt.Errorf("the specified EndState '%s' is not allowed to have transitions", f.EndState) } - // TODO: Additional validation for unique State and Method names,... + // TODO: Additional validation for unique State and Action names,... return nil } @@ -162,7 +162,7 @@ func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResu } // Otherwise, update an existing Flow. - return executeFlowMethod(db, *f, executionOptions) + return executeFlowAction(db, *f, executionOptions) } // ResultFromError returns an error response for the Flow. diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index 4c449e627..a3003a6ad 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -10,8 +10,8 @@ type InitializationSchema interface { AddInputs(inputList ...Input) } -// MethodExecutionSchema represents an interface for managing method execution schemas. -type MethodExecutionSchema interface { +// ExecutionSchema represents an interface for managing method execution schemas. +type ExecutionSchema interface { Get(path string) gjson.Result Set(path string, value interface{}) error SetError(inputName string, inputError InputError) @@ -37,8 +37,8 @@ type defaultSchema struct { outputData jsonmanager.JSONManager } -// newSchemaWithInputData creates a new MethodExecutionSchema with input data. -func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) MethodExecutionSchema { +// newSchemaWithInputData creates a new ExecutionSchema with input data. +func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) ExecutionSchema { outputData := jsonmanager.NewJSONManager() return &defaultSchema{ inputData: inputData, @@ -46,8 +46,8 @@ func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) MethodExe } } -// newSchemaWithInputData creates a new MethodExecutionSchema with input data. -func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (MethodExecutionSchema, error) { +// newSchemaWithInputData creates a new ExecutionSchema with input data. +func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (ExecutionSchema, error) { data, err := jsonmanager.NewJSONManagerFromString(outputData.String()) if err != nil { return nil, err @@ -59,13 +59,13 @@ func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (Method }, nil } -// newSchema creates a new MethodExecutionSchema with no input data. -func newSchema() MethodExecutionSchema { +// newSchema creates a new ExecutionSchema with no input data. +func newSchema() ExecutionSchema { inputData := jsonmanager.NewJSONManager() return newSchemaWithInputData(inputData) } -// toInitializationSchema converts MethodExecutionSchema to InitializationSchema. +// toInitializationSchema converts ExecutionSchema to InitializationSchema. func (s *defaultSchema) toInitializationSchema() InitializationSchema { return s } diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index 348aeb7e7..d14a0f17a 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -6,19 +6,19 @@ import ( "net/http" ) -// Link represents a link to an action. -type Link struct { +// PublicAction represents a link to an action. +type PublicAction struct { Href string `json:"href"` Inputs PublicSchema `json:"inputs"` - MethodName MethodName `json:"method_name"` + ActionName ActionName `json:"action_name"` Description string `json:"description"` } -// Links is a collection of Link instances. -type Links []Link +// PublicActions is a collection of PublicAction instances. +type PublicActions []PublicAction -// Add adds a link to the collection of Links. -func (ls *Links) Add(l Link) { +// Add adds a link to the collection of PublicActions. +func (ls *PublicActions) Add(l PublicAction) { *ls = append(*ls, l) } @@ -43,11 +43,11 @@ type PublicInput struct { // PublicResponse represents the response of an action execution. type PublicResponse struct { - State StateName `json:"state"` - Status int `json:"status"` - Payload interface{} `json:"payload,omitempty"` - Links Links `json:"links"` - Error *PublicError `json:"error,omitempty"` + State StateName `json:"state"` + Status int `json:"status"` + Payload interface{} `json:"payload,omitempty"` + Actions PublicActions `json:"actions"` + Error *PublicError `json:"error,omitempty"` } // FlowResult interface defines methods for obtaining response and status. @@ -87,10 +87,10 @@ func (r DefaultFlowResult) Status() int { return r.PublicResponse.Status } -// methodExecutionResult holds the result of a method execution. -type methodExecutionResult struct { - methodName MethodName - schema MethodExecutionSchema +// actionExecutionResult holds the result of a method execution. +type actionExecutionResult struct { + actionName ActionName + schema ExecutionSchema } // executionResult holds the result of an action execution. @@ -98,20 +98,20 @@ type executionResult struct { nextState StateName flowError FlowError - *methodExecutionResult + *actionExecutionResult } // generateResponse generates a response based on the execution result. func (er *executionResult) generateResponse(fc defaultFlowContext) FlowResult { - // Generate links for the response. - links := er.generateLinks(fc) + // Generate actions for the response. + actions := er.generateActions(fc) // Create the response object. resp := PublicResponse{ State: er.nextState, Status: http.StatusOK, Payload: fc.payload.Unmarshal(), - Links: links, + Actions: actions, } // Include flow error if present. @@ -126,51 +126,51 @@ func (er *executionResult) generateResponse(fc defaultFlowContext) FlowResult { return newFlowResultFromResponse(resp) } -// generateLinks generates a collection of links based on the execution result. -func (er *executionResult) generateLinks(fc defaultFlowContext) Links { - var links Links +// generateActions generates a collection of links based on the execution result. +func (er *executionResult) generateActions(fc defaultFlowContext) PublicActions { + var actions PublicActions // Get transitions for the next state. transitions := fc.flow.getTransitionsForState(er.nextState) if transitions != nil { for _, t := range *transitions { - currentMethodName := t.Method.GetName() - currentDescription := t.Method.GetDescription() + currentActionName := t.Action.GetName() + currentDescription := t.Action.GetDescription() - // Create link HREF based on the current flow context and method name. - href := er.createHref(fc, currentMethodName) - schema := er.getExecutionSchema(currentMethodName) + // Create action HREF based on the current flow context and method name. + href := er.createHref(fc, currentActionName) + schema := er.getExecutionSchema(currentActionName) if schema == nil { // Create schema if not available. - if schema = er.createSchema(fc, t.Method); schema == nil { + if schema = er.createSchema(fc, t.Action); schema == nil { continue } } - // Create the link instance. - link := Link{ + // Create the action instance. + action := PublicAction{ Href: href, Inputs: schema.toPublicSchema(er.nextState), - MethodName: currentMethodName, + ActionName: currentActionName, Description: currentDescription, } - links.Add(link) + actions.Add(action) } } - return links + return actions } // createSchema creates an execution schema for a method if needed. -func (er *executionResult) createSchema(fc defaultFlowContext, method Method) MethodExecutionSchema { - var schema MethodExecutionSchema +func (er *executionResult) createSchema(fc defaultFlowContext, method Action) ExecutionSchema { + var schema ExecutionSchema var err error - if er.methodExecutionResult != nil { - data := er.methodExecutionResult.schema.getOutputData() + if er.actionExecutionResult != nil { + data := er.actionExecutionResult.schema.getOutputData() schema, err = newSchemaWithOutputData(data) } else { schema = newSchema() @@ -181,7 +181,7 @@ func (er *executionResult) createSchema(fc defaultFlowContext, method Method) Me } // Initialize the method. - mic := defaultMethodInitializationContext{schema: schema.toInitializationSchema(), stash: fc.stash} + mic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: fc.stash} method.Initialize(&mic) if mic.isSuspended { @@ -192,16 +192,16 @@ func (er *executionResult) createSchema(fc defaultFlowContext, method Method) Me } // getExecutionSchema gets the execution schema for a given method name. -func (er *executionResult) getExecutionSchema(methodName MethodName) MethodExecutionSchema { - if er.methodExecutionResult == nil || methodName != er.methodExecutionResult.methodName { +func (er *executionResult) getExecutionSchema(methodName ActionName) ExecutionSchema { + if er.actionExecutionResult == nil || methodName != er.actionExecutionResult.actionName { return nil } - return er.methodExecutionResult.schema + return er.actionExecutionResult.schema } // createHref creates a link HREF based on the current flow context and method name. -func (er *executionResult) createHref(fc defaultFlowContext, methodName MethodName) string { +func (er *executionResult) createHref(fc defaultFlowContext, methodName ActionName) string { action := utils.CreateActionParam(string(methodName), fc.GetFlowID()) return fmt.Sprintf("%s?flowpilot_action=%s", fc.GetPath(), action) } diff --git a/backend/flowpilot/utils/param.go b/backend/flowpilot/utils/param.go index 465f8103d..67c7fc3a5 100644 --- a/backend/flowpilot/utils/param.go +++ b/backend/flowpilot/utils/param.go @@ -8,25 +8,25 @@ import ( // ParsedAction represents a parsed action from an input string. type ParsedAction struct { - MethodName string // The name of the method extracted from the input string. + ActionName string // The name of the action extracted from the input string. FlowID uuid.UUID // The UUID representing the flow ID extracted from the input string. } -// ParseActionParam parses an input string to extract method name and flow ID. +// ParseActionParam parses an input string to extract action name and flow ID. func ParseActionParam(inputString string) (*ParsedAction, error) { if inputString == "" { return nil, fmt.Errorf("input string is empty") } - // Split the input string into method and flow ID parts using "@" as separator. + // Split the input string into action and flow ID parts using "@" as separator. parts := strings.SplitN(inputString, "@", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid input string format") } - // Extract method name from the first part of the split. - method := parts[0] - if len(method) == 0 { + // Extract action name from the first part of the split. + action := parts[0] + if len(action) == 0 { return nil, fmt.Errorf("first part of input string is empty") } @@ -36,11 +36,11 @@ func ParseActionParam(inputString string) (*ParsedAction, error) { return nil, fmt.Errorf("failed to parse second part of the input string: %w", err) } - // Return a ParsedAction instance with extracted method name and flow ID. - return &ParsedAction{MethodName: method, FlowID: flowID}, nil + // Return a ParsedAction instance with extracted action name and flow ID. + return &ParsedAction{ActionName: action, FlowID: flowID}, nil } -// CreateActionParam creates an input string from method name and flow ID. -func CreateActionParam(method string, flowID uuid.UUID) string { - return fmt.Sprintf("%s@%s", method, flowID) +// CreateActionParam creates an input string from action name and flow ID. +func CreateActionParam(action string, flowID uuid.UUID) string { + return fmt.Sprintf("%s@%s", action, flowID) } diff --git a/backend/persistence/migrations/20230810173315_create_flows.up.fizz b/backend/persistence/migrations/20230810173315_create_flows.up.fizz index 56a9a7a10..82f2f3722 100644 --- a/backend/persistence/migrations/20230810173315_create_flows.up.fizz +++ b/backend/persistence/migrations/20230810173315_create_flows.up.fizz @@ -12,11 +12,11 @@ create_table("flows") { create_table("transitions") { t.Column("id", "uuid", {primary: true}) t.Column("flow_id", "uuid") - t.Column("method", "string") + t.Column("action", "string") t.Column("from_state", "string") t.Column("to_state", "string") t.Column("input_data", "string") t.Column("error_code", "string", {"null": true}) t.ForeignKey("flow_id", {"flows": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) t.Timestamps() -} \ No newline at end of file +} diff --git a/backend/persistence/models/flowdb.go b/backend/persistence/models/flowdb.go index e94bcf135..16929a3a5 100644 --- a/backend/persistence/models/flowdb.go +++ b/backend/persistence/models/flowdb.go @@ -144,7 +144,7 @@ func (flowDB FlowDB) CreateTransition(transitionModel flowpilot.TransitionModel) t := Transition{ ID: transitionModel.ID, FlowID: transitionModel.FlowID, - Method: string(transitionModel.Method), + Action: string(transitionModel.Action), FromState: string(transitionModel.FromState), ToState: string(transitionModel.ToState), InputData: transitionModel.InputData, @@ -161,11 +161,11 @@ func (flowDB FlowDB) CreateTransition(transitionModel flowpilot.TransitionModel) return nil } -func (flowDB FlowDB) FindLastTransitionWithMethod(flowID uuid.UUID, method flowpilot.MethodName) (*flowpilot.TransitionModel, error) { +func (flowDB FlowDB) FindLastTransitionWithAction(flowID uuid.UUID, actionName flowpilot.ActionName) (*flowpilot.TransitionModel, error) { var transitionModel Transition err := flowDB.tx.Where("flow_id = ?", flowID). - Where("method = ?", method). + Where("action = ?", actionName). Order("created_at desc"). First(&transitionModel) if err != nil { diff --git a/backend/persistence/models/transition.go b/backend/persistence/models/transition.go index 1d337a416..42ae8c401 100644 --- a/backend/persistence/models/transition.go +++ b/backend/persistence/models/transition.go @@ -12,7 +12,7 @@ import ( type Transition struct { ID uuid.UUID `json:"id" db:"id"` FlowID uuid.UUID `json:"-" db:"flow_id" ` - Method string `json:"method" db:"method"` + Action string `json:"action" db:"action"` FromState string `json:"from_state" db:"from_state"` ToState string `json:"to_state" db:"to_state"` InputData string `json:"input_data" db:"input_data"` @@ -26,7 +26,7 @@ func (t *Transition) ToFlowpilotModel() *flowpilot.TransitionModel { return &flowpilot.TransitionModel{ ID: t.ID, FlowID: t.FlowID, - Method: flowpilot.MethodName(t.Method), + Action: flowpilot.ActionName(t.Action), FromState: flowpilot.StateName(t.FromState), ToState: flowpilot.StateName(t.ToState), InputData: t.InputData, From 3c46a9c0ddd2e0a7c7ac3b04a01948be7b4fc79e Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 5 Oct 2023 14:33:25 +0200 Subject: [PATCH 010/278] introduce sub-flows --- backend/flow_api_test/flow.go | 2 +- .../flow_api_test/static/generic_client.html | 12 +- backend/flowpilot/builder.go | 210 ++++++++++++++--- backend/flowpilot/context.go | 130 ++++++----- backend/flowpilot/context_action_exec.go | 214 ++++++++++++------ backend/flowpilot/context_action_init.go | 24 +- backend/flowpilot/context_flow.go | 56 +++-- backend/flowpilot/db.go | 106 +++++---- backend/flowpilot/errors.go | 55 +++-- backend/flowpilot/flow.go | 193 +++++++++++----- backend/flowpilot/input.go | 46 ++-- backend/flowpilot/input_schema.go | 75 +++--- backend/flowpilot/response.go | 137 +++++------ backend/flowpilot/utils/storage.go | 50 ++++ .../20230810173315_create_flows.up.fizz | 1 - backend/persistence/models/flow.go | 22 +- backend/persistence/models/flowdb.go | 24 +- 17 files changed, 855 insertions(+), 502 deletions(-) create mode 100644 backend/flowpilot/utils/storage.go diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index 1710fc81c..20aadb556 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -23,4 +23,4 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). TTL(time.Minute * 10). Debug(true). - Build() + MustBuild() diff --git a/backend/flow_api_test/static/generic_client.html b/backend/flow_api_test/static/generic_client.html index 5ab26906e..a2988a951 100644 --- a/backend/flow_api_test/static/generic_client.html +++ b/backend/flow_api_test/static/generic_client.html @@ -119,7 +119,7 @@

✨ Login

form.method = 'POST'; const actionHeadline = document.createElement('h4'); - actionHeadline.textContent = "⚡ Action: " + action.action_name; + actionHeadline.textContent = "⚡ Action: " + action.action; form.appendChild(actionHeadline); @@ -131,14 +131,14 @@

✨ Login

description.textContent = action.description; form.appendChild(description); - const inputsHeadline = document.createElement('h5'); - inputsHeadline.textContent = "⛳ Inputs"; - form.appendChild(inputsHeadline); + const schemaHeadline = document.createElement('h5'); + schemaHeadline.textContent = "⛳ Schema"; + form.appendChild(schemaHeadline); - if (action.inputs) { + if (action.schema) { const inputList = document.createElement('ul'); - action.inputs.forEach(input => { + action.schema.forEach(input => { const inputListItem = document.createElement('li'); const label = document.createElement("label"); diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 5bdc1b0b9..fb2eacfc1 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -1,67 +1,217 @@ package flowpilot import ( + "fmt" "time" ) -// FlowBuilder is a builder struct for creating a new Flow. -type FlowBuilder struct { - path string - ttl time.Duration - initialState StateName - errorState StateName - endState StateName +type FlowBuilder interface { + TTL(ttl time.Duration) FlowBuilder + State(state StateName, actions ...Action) FlowBuilder + FixedStates(initialState, errorState, finalState StateName) FlowBuilder + Debug(enabled bool) FlowBuilder + SubFlows(subFlows ...SubFlow) FlowBuilder + Build() (Flow, error) + MustBuild() Flow +} + +type SubFlowBuilder interface { + State(state StateName, actions ...Action) SubFlowBuilder + SubFlows(subFlows ...SubFlow) SubFlowBuilder + FixedStates(initialState StateName) SubFlowBuilder + Build() (SubFlow, error) + MustBuild() SubFlow +} + +// defaultFlowBuilderBase is the base flow builder struct. +type defaultFlowBuilderBase struct { flow StateTransitions - debug bool + subFlows SubFlows + initialState StateName + stateDetails stateDetails } -// NewFlow creates a new FlowBuilder that builds a new flow available under the specified path. -func NewFlow(path string) *FlowBuilder { - return &FlowBuilder{ - path: path, - flow: make(StateTransitions), - } +// defaultFlowBuilder is a builder struct for creating a new Flow. +type defaultFlowBuilder struct { + path string + ttl time.Duration + errorState StateName + endState StateName + debug bool + + defaultFlowBuilderBase +} + +// newFlowBuilderBase creates a new defaultFlowBuilderBase instance. +func newFlowBuilderBase() defaultFlowBuilderBase { + return defaultFlowBuilderBase{flow: make(StateTransitions), subFlows: make(SubFlows), stateDetails: make(stateDetails)} +} + +// NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path. +func NewFlow(path string) FlowBuilder { + fbBase := newFlowBuilderBase() + + return &defaultFlowBuilder{path: path, defaultFlowBuilderBase: fbBase} } // TTL sets the time-to-live (TTL) for the flow. -func (fb *FlowBuilder) TTL(ttl time.Duration) *FlowBuilder { +func (fb *defaultFlowBuilder) TTL(ttl time.Duration) FlowBuilder { fb.ttl = ttl return fb } -// State adds a new state transition to the FlowBuilder. -func (fb *FlowBuilder) State(state StateName, mList ...Action) *FlowBuilder { +func (fb *defaultFlowBuilderBase) addState(state StateName, actions ...Action) { var transitions Transitions - for _, m := range mList { - transitions = append(transitions, Transition{Action: m}) + + for _, action := range actions { + transitions = append(transitions, Transition{Action: action}) } + fb.flow[state] = transitions +} + +func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...SubFlow) { + for _, subFlow := range subFlows { + initialState := subFlow.getInitialState() + fb.subFlows[initialState] = subFlow + } +} + +func (fb *defaultFlowBuilderBase) addDefaultStates(states ...StateName) { + for _, state := range states { + if _, ok := fb.flow[state]; !ok { + fb.addState(state) + } + } +} + +// State adds a new transition to the flow. +func (fb *defaultFlowBuilder) State(state StateName, actions ...Action) FlowBuilder { + fb.addState(state, actions...) return fb } // FixedStates sets the initial and final states of the flow. -func (fb *FlowBuilder) FixedStates(initialState, errorState, finalState StateName) *FlowBuilder { +func (fb *defaultFlowBuilder) FixedStates(initialState, errorState, finalState StateName) FlowBuilder { fb.initialState = initialState fb.errorState = errorState fb.endState = finalState 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 *FlowBuilder) Debug(enabled bool) *FlowBuilder { +func (fb *defaultFlowBuilder) Debug(enabled bool) FlowBuilder { fb.debug = enabled return fb } +func (fb *defaultFlowBuilder) scanStateActions(flow StateTransitions, subFlows SubFlows) error { + for state, transitions := range flow { + if _, ok := fb.stateDetails[state]; ok { + return fmt.Errorf("flow state '%s' is not unique", state) + } + + actions := transitions.getActions() + + fb.stateDetails[state] = stateDetail{ + flow: flow, + subFlows: subFlows, + actions: actions, + } + } + + for _, sf := range subFlows { + if err := fb.scanStateActions(sf.getFlow(), sf.getSubFlows()); err != nil { + return err + } + } + + return nil +} + // Build constructs and returns the Flow object. -func (fb *FlowBuilder) Build() Flow { - return Flow{ - Path: fb.path, - Flow: fb.flow, - InitialState: fb.initialState, - ErrorState: fb.errorState, - EndState: fb.endState, - TTL: fb.ttl, - Debug: fb.debug, +func (fb *defaultFlowBuilder) Build() (Flow, error) { + fb.addDefaultStates(fb.initialState, fb.errorState, fb.endState) + + if err := fb.scanStateActions(fb.flow, fb.subFlows); err != nil { + return nil, err } + + return &defaultFlow{ + path: fb.path, + flow: fb.flow, + initialState: fb.initialState, + errorState: fb.errorState, + endState: fb.endState, + subFlows: fb.subFlows, + stateDetails: fb.stateDetails, + ttl: fb.ttl, + debug: fb.debug, + }, 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 +} + +// defaultFlowBuilder is a builder struct for creating a new SubFlow. +type defaultSubFlowBuilder struct { + defaultFlowBuilderBase +} + +// NewSubFlow creates a new SubFlowBuilder. +func NewSubFlow() SubFlowBuilder { + fbBase := newFlowBuilderBase() + return &defaultSubFlowBuilder{defaultFlowBuilderBase: fbBase} +} + +func (sfb *defaultSubFlowBuilder) SubFlows(subFlows ...SubFlow) SubFlowBuilder { + sfb.addSubFlows(subFlows...) + return sfb +} + +// State adds a new transition to the flow. +func (sfb *defaultSubFlowBuilder) State(state StateName, actions ...Action) SubFlowBuilder { + sfb.addState(state, actions...) + return sfb +} + +// FixedStates sets the initial of the sub-flow. +func (sfb *defaultSubFlowBuilder) FixedStates(initialState StateName) SubFlowBuilder { + sfb.initialState = initialState + return sfb +} + +// Build constructs and returns the SubFlow object. +func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { + sfb.addDefaultStates(sfb.initialState) + + return &defaultFlow{ + flow: sfb.flow, + initialState: sfb.initialState, + subFlows: sfb.subFlows, + }, nil +} + +// MustBuild constructs and returns the SubFlow object, panics on error. +func (sfb *defaultSubFlowBuilder) MustBuild() SubFlow { + sf, err := sfb.Build() + + if err != nil { + panic(err) + } + + return sf } diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 5adc6cdfd..2989ad34d 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -5,48 +5,47 @@ import ( "errors" "fmt" "github.com/gofrs/uuid" - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" "github.com/teamhanko/hanko/backend/flowpilot/utils" "time" ) -// flowContext represents the basic context for a Flow. +// flowContext represents the basic context for a flow. type flowContext interface { - // GetFlowID returns the unique ID of the current Flow. + // GetFlowID returns the unique ID of the current defaultFlow. GetFlowID() uuid.UUID - // GetPath returns the current path within the Flow. + // GetPath returns the current path within the flow. GetPath() string // Payload returns the JSONManager for accessing payload data. - Payload() jsonmanager.JSONManager + Payload() utils.Payload // Stash returns the JSONManager for accessing stash data. - Stash() jsonmanager.JSONManager - // GetInitialState returns the initial state of the Flow. + Stash() utils.Stash + // GetInitialState returns the initial state of the flow. GetInitialState() StateName - // GetCurrentState returns the current state of the Flow. + // GetCurrentState returns the current state of the flow. GetCurrentState() StateName // CurrentStateEquals returns true, when one of the given states matches the current state. CurrentStateEquals(states ...StateName) bool - // GetPreviousState returns the previous state of the Flow. + // GetPreviousState returns the previous state of the flow. GetPreviousState() *StateName - // GetErrorState returns the designated error state of the Flow. + // GetErrorState returns the designated error state of the flow. GetErrorState() StateName - // GetEndState returns the final state of the Flow. + // GetEndState returns the final state of the flow. GetEndState() StateName - // StateExists checks if a given state exists within the Flow. + // StateExists checks if a given state exists within the flow. StateExists(stateName StateName) bool } -// actionInitializationContext represents the basic context for a Flow action's initialization. +// actionInitializationContext represents the basic context for a flow action's initialization. type actionInitializationContext interface { // AddInputs adds input parameters to the schema. - AddInputs(inputList ...Input) + AddInputs(inputs ...Input) // Stash returns the ReadOnlyJSONManager for accessing stash data. - Stash() jsonmanager.ReadOnlyJSONManager + Stash() utils.Stash // SuspendAction suspends the current action's execution. SuspendAction() } -// actionExecutionContext represents the context for a action execution. +// actionExecutionContext represents the context for an action execution. type actionExecutionContext interface { flowContext // Input returns the ExecutionSchema for the action. @@ -55,7 +54,7 @@ type actionExecutionContext interface { // TODO: FetchActionInput (for a action name) is maybe useless and can be removed or replaced. // FetchActionInput fetches input data for a specific action. - FetchActionInput(actionName ActionName) (jsonmanager.ReadOnlyJSONManager, error) + FetchActionInput(actionName ActionName) (utils.ReadOnlyActionInput, error) // ValidateInputData validates the input data against the schema. ValidateInputData() bool // CopyInputValuesToStash copies specified inputs to the stash. @@ -65,20 +64,23 @@ type actionExecutionContext interface { // actionExecutionContinuationContext represents the context within an action continuation. type actionExecutionContinuationContext interface { actionExecutionContext - // ContinueFlow continues the Flow execution to the specified next state. + // ContinueFlow continues the flow execution to the specified next state. ContinueFlow(nextState StateName) error - // ContinueFlowWithError continues the Flow execution to the specified next state with an error. + // ContinueFlowWithError continues the flow execution to the specified next state with an error. ContinueFlowWithError(nextState StateName, flowErr FlowError) error - + // StartSubFlow starts a sub-flow and continues the flow execution to the specified next states after the sub-flow has been ended. + StartSubFlow(initState StateName, nextStates ...StateName) error + // EndSubFlow ends the sub-flow and continues the flow execution to the previously specified next states. + EndSubFlow() error // TODO: Implement a function to step back to the previous state (while skipping self-transitions and recalling preserved data). } -// InitializationContext is a shorthand for actionInitializationContext within flow actions. +// InitializationContext is a shorthand for actionInitializationContext within the flow initialization method. type InitializationContext interface { actionInitializationContext } -// ExecutionContext is a shorthand for actionExecutionContinuationContext within flow actions. +// ExecutionContext is a shorthand for actionExecutionContinuationContext within flow execution method. type ExecutionContext interface { actionExecutionContinuationContext } @@ -95,25 +97,22 @@ type PluginAfterActionExecutionContext interface { actionExecutionContext } -// createAndInitializeFlow initializes the Flow and returns a flow Response. -func createAndInitializeFlow(db FlowDB, flow Flow) (FlowResult, error) { +// createAndInitializeFlow initializes the flow and returns a flow Response. +func createAndInitializeFlow(db FlowDB, flow defaultFlow) (FlowResult, error) { // Wrap the provided FlowDB with additional functionality. dbw := wrapDB(db) - // Calculate the expiration time for the Flow. - expiresAt := time.Now().Add(flow.TTL).UTC() - - // TODO: Consider implementing types for stash and payload that extend "jsonmanager.NewJSONManager()". - // This could enhance the code structure and provide clearer interfaces for handling these data structures. + // Calculate the expiration time for the flow. + expiresAt := time.Now().Add(flow.ttl).UTC() // Initialize JSONManagers for stash and payload. - stash := jsonmanager.NewJSONManager() - payload := jsonmanager.NewJSONManager() + stash := utils.NewStash() + payload := utils.NewPayload() - // Create a new Flow model with the provided parameters. - p := flowCreationParam{currentState: flow.InitialState, expiresAt: expiresAt} - flowModel, err := dbw.CreateFlowWithParam(p) + // Create a new flow model with the provided parameters. + flowCreation := flowCreationParam{currentState: flow.initialState, expiresAt: expiresAt} + flowModel, err := dbw.CreateFlowWithParam(flowCreation) if err != nil { - return nil, fmt.Errorf("failed to create Flow: %w", err) + return nil, fmt.Errorf("failed to create flow: %w", err) } // Create a defaultFlowContext instance. @@ -128,39 +127,39 @@ func createAndInitializeFlow(db FlowDB, flow Flow) (FlowResult, error) { // Generate a response based on the execution result. er := executionResult{nextState: flowModel.CurrentState} - return er.generateResponse(fc), nil + return er.generateResponse(fc, flow.debug), nil } -// executeFlowAction processes the Flow and returns a Response. -func executeFlowAction(db FlowDB, flow Flow, options flowExecutionOptions) (FlowResult, error) { - // Parse the actionParam parameter to get the actionParam name and Flow ID. +// executeFlowAction processes the flow and returns a Response. +func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions) (FlowResult, error) { + // Parse the actionParam parameter to get the actionParam name and flow ID. actionParam, err := utils.ParseActionParam(options.action) if err != nil { - return newFlowResultFromError(flow.ErrorState, ErrorActionParamInvalid.Wrap(err), flow.Debug), nil + return newFlowResultFromError(flow.errorState, ErrorActionParamInvalid.Wrap(err), flow.debug), nil } - // Retrieve the Flow model from the database using the Flow ID. + // Retrieve the flow model from the database using the flow ID. flowModel, err := db.GetFlow(actionParam.FlowID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil + return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil } - return nil, fmt.Errorf("failed to get Flow: %w", err) + return nil, fmt.Errorf("failed to get flow: %w", err) } - // Check if the Flow has expired. + // Check if the flow has expired. if time.Now().After(flowModel.ExpiresAt) { - return newFlowResultFromError(flow.ErrorState, ErrorFlowExpired, flow.Debug), nil + return newFlowResultFromError(flow.errorState, ErrorFlowExpired, flow.debug), nil } - // Parse stash data from the Flow model. - stash, err := jsonmanager.NewJSONManagerFromString(flowModel.StashData) + // Parse stash data from the flow model. + stash, err := utils.NewStashFromString(flowModel.StashData) if err != nil { - return nil, fmt.Errorf("failed to parse stash from flowModel: %w", err) + return nil, fmt.Errorf("failed to parse stash from flow: %w", err) } // Initialize JSONManagers for payload and flash data. - payload := jsonmanager.NewJSONManager() + payload := utils.NewPayload() // Create a defaultFlowContext instance. fc := defaultFlowContext{ @@ -171,58 +170,57 @@ func executeFlowAction(db FlowDB, flow Flow, options flowExecutionOptions) (Flow payload: payload, } - // Get the available transitions for the current state. - transitions := fc.getCurrentTransitions() - if transitions == nil { - err2 := errors.New("the state does not allow to continue with the flow") - return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err2), flow.Debug), nil + detail, err := flow.getStateDetail(flowModel.CurrentState) + if err != nil { + return nil, err } // Parse raw input data into JSONManager. raw := options.inputData.getJSONStringOrDefault() - inputJSON, err := jsonmanager.NewJSONManagerFromString(raw) + inputJSON, err := utils.NewActionInputFromString(raw) if err != nil { return nil, fmt.Errorf("failed to parse input data: %w", err) } // Create a ActionName from the parsed actionParam name. actionName := ActionName(actionParam.ActionName) + // Get the action associated with the actionParam name. - action, err := transitions.getAction(actionName) + action, err := detail.actions.getByName(actionName) if err != nil { - return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted.Wrap(err), flow.Debug), nil + return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil } // Initialize the schema and action context for action execution. schema := newSchemaWithInputData(inputJSON) - mic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: stash} - action.Initialize(&mic) + aic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: stash} + action.Initialize(&aic) // Check if the action is suspended. - if mic.isSuspended { - return newFlowResultFromError(flow.ErrorState, ErrorOperationNotPermitted, flow.Debug), nil + if aic.isSuspended { + return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted, flow.debug), nil } // Create a actionExecutionContext instance for action execution. - mec := defaultActionExecutionContext{ + aec := defaultActionExecutionContext{ actionName: actionName, input: schema, defaultFlowContext: fc, } // Execute the action and handle any errors. - err = action.Execute(&mec) + err = action.Execute(&aec) if err != nil { return nil, fmt.Errorf("the action failed to handle the request: %w", err) } // Ensure that the action has set a result object. - if mec.executionResult == nil { + if aec.executionResult == nil { return nil, errors.New("the action has not set a result object") } // Generate a response based on the execution result. - er := *mec.executionResult + er := *aec.executionResult - return er.generateResponse(fc), nil + return er.generateResponse(fc, flow.debug), nil } diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 9cd4c3865..96a7f8042 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -3,7 +3,7 @@ package flowpilot import ( "errors" "fmt" - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // defaultActionExecutionContext is the default implementation of the actionExecutionContext interface. @@ -15,105 +15,103 @@ type defaultActionExecutionContext struct { defaultFlowContext // Embedding the defaultFlowContext for common context fields. } -// saveNextState updates the Flow's state and saves Transition data after action execution. -func (mec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { - currentState := mec.flowModel.CurrentState - previousState := mec.flowModel.PreviousState - - // Update the previous state only if the next state is different from the current state. - if executionResult.nextState != currentState { - previousState = ¤tState - } - - completed := executionResult.nextState == mec.flow.EndState - newVersion := mec.flowModel.Version + 1 - - // Prepare parameters for updating the Flow in the database. - flowUpdateParam := flowUpdateParam{ - flowID: mec.flowModel.ID, - nextState: executionResult.nextState, - previousState: previousState, - stashData: mec.stash.String(), - version: newVersion, - completed: completed, - expiresAt: mec.flowModel.ExpiresAt, - createdAt: mec.flowModel.CreatedAt, +// saveNextState updates the flow's state and saves Transition data after action execution. +func (aec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { + completed := executionResult.nextState == aec.flow.endState + newVersion := aec.flowModel.Version + 1 + stashData := aec.stash.String() + + // Prepare parameters for updating the flow in the database. + flowUpdate := flowUpdateParam{ + flowID: aec.flowModel.ID, + nextState: executionResult.nextState, + stashData: stashData, + version: newVersion, + completed: completed, + expiresAt: aec.flowModel.ExpiresAt, + createdAt: aec.flowModel.CreatedAt, } - // Update the Flow model in the database. - flowModel, err := mec.dbw.UpdateFlowWithParam(flowUpdateParam) - if err != nil { + // Update the flow model in the database. + if _, err := aec.dbw.UpdateFlowWithParam(flowUpdate); err != nil { return fmt.Errorf("failed to store updated flow: %w", err) } - mec.flowModel = *flowModel - // Get the data to persists from the executed action schema for recording. - inputDataToPersist := mec.input.getDataToPersist().String() + inputDataToPersist := aec.input.getDataToPersist().String() // Prepare parameters for creating a new Transition in the database. - transitionCreationParam := transitionCreationParam{ - flowID: mec.flowModel.ID, - actionName: mec.actionName, - fromState: currentState, + transitionCreation := transitionCreationParam{ + flowID: aec.flowModel.ID, + actionName: aec.actionName, + fromState: aec.flowModel.CurrentState, toState: executionResult.nextState, inputData: inputDataToPersist, flowError: executionResult.flowError, } // Create a new Transition in the database. - _, err = mec.dbw.CreateTransitionWithParam(transitionCreationParam) - if err != nil { + if _, err := aec.dbw.CreateTransitionWithParam(transitionCreation); err != nil { return fmt.Errorf("failed to store a new transition: %w", err) } return nil } -// continueFlow continues the Flow execution to the specified nextState with an optional error type. -func (mec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { - // Check if the specified nextState is valid. - if exists := mec.flow.stateExists(nextState); !exists { - return errors.New("the execution result contains an invalid state") +// continueFlow continues the flow execution to the specified nextState with an optional error type. +func (aec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError, skipFlowValidation bool) error { + detail, err := aec.flow.getStateDetail(aec.flowModel.CurrentState) + if err != nil { + return err + } + + if !skipFlowValidation { + // Check if the specified nextState is valid. + if _, ok := detail.flow[nextState]; !(ok || + detail.subFlows.isEntryStateAllowed(nextState) || + nextState == aec.flow.endState || + nextState == aec.flow.errorState) { + return fmt.Errorf("progression to the specified state '%s' is not allowed", nextState) + } + } + + // Prepare the result for continuing the flow. + actionResult := actionExecutionResult{ + actionName: aec.actionName, + schema: aec.input, } - // Prepare an executionResult for continuing the Flow. result := executionResult{ - nextState: nextState, - flowError: flowError, - actionExecutionResult: &actionExecutionResult{ - actionName: mec.actionName, - schema: mec.input, - }, + nextState: nextState, + flowError: flowError, + actionExecutionResult: &actionResult, } // Save the next state and transition data. - err := mec.saveNextState(result) - if err != nil { + if err := aec.saveNextState(result); err != nil { return fmt.Errorf("failed to save the transition data: %w", err) } - mec.executionResult = &result + aec.executionResult = &result return nil } // Input returns the ExecutionSchema for accessing input data. -func (mec *defaultActionExecutionContext) Input() ExecutionSchema { - return mec.input +func (aec *defaultActionExecutionContext) Input() ExecutionSchema { + return aec.input } // Payload returns the JSONManager for accessing payload data. -func (mec *defaultActionExecutionContext) Payload() jsonmanager.JSONManager { - return mec.payload +func (aec *defaultActionExecutionContext) Payload() utils.Payload { + return aec.payload } // CopyInputValuesToStash copies specified inputs to the stash. -func (mec *defaultActionExecutionContext) CopyInputValuesToStash(inputNames ...string) error { +func (aec *defaultActionExecutionContext) CopyInputValuesToStash(inputNames ...string) error { for _, inputName := range inputNames { // Copy input values to the stash. - err := mec.stash.Set(inputName, mec.input.Get(inputName).Value()) - if err != nil { + if err := aec.stash.Set(inputName, aec.input.Get(inputName).Value()); err != nil { return err } } @@ -121,16 +119,100 @@ func (mec *defaultActionExecutionContext) CopyInputValuesToStash(inputNames ...s } // ValidateInputData validates the input data against the schema. -func (mec *defaultActionExecutionContext) ValidateInputData() bool { - return mec.input.validateInputData(mec.flowModel.CurrentState, mec.stash) +func (aec *defaultActionExecutionContext) ValidateInputData() bool { + return aec.input.validateInputData(aec.flowModel.CurrentState, aec.stash) } -// ContinueFlow continues the Flow execution to the specified nextState. -func (mec *defaultActionExecutionContext) ContinueFlow(nextState StateName) error { - return mec.continueFlow(nextState, nil) +// ContinueFlow continues the flow execution to the specified nextState. +func (aec *defaultActionExecutionContext) ContinueFlow(nextState StateName) error { + return aec.continueFlow(nextState, nil, false) +} + +// ContinueFlowWithError continues the flow execution to the specified nextState with an error type. +func (aec *defaultActionExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { + return aec.continueFlow(nextState, flowErr, false) +} + +// StartSubFlow initiates the sub-flow associated with the specified StateName of the entry state (first parameter). +// After a sub-flow action calls EndSubFlow(), the flow progresses to a state within the current flow or another +// sub-flow's entry state, as specified in the list of nextStates (every StateName passed after the first parameter). +func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nextStates ...StateName) error { + detail, err := aec.flow.getStateDetail(aec.flowModel.CurrentState) + if err != nil { + return err + } + + // the specified entry state must be an entry state to a sub-flow of the current flow + if entryStateAllowed := detail.subFlows.isEntryStateAllowed(entryState); !entryStateAllowed { + return errors.New("the specified entry state is not associated with a sub-flow of the current flow") + } + + var scheduledStates []StateName + + for index, nextState := range nextStates { + subFlowEntryStateAllowed := detail.subFlows.isEntryStateAllowed(nextState) + + // validate the current next state + if index == len(nextStates)-1 { + // the last state must be a member of the current flow or a sub-flow entry state + if _, ok := detail.flow[nextState]; !ok && !subFlowEntryStateAllowed { + return errors.New("the last next state is not a sub-flow entry state or a state associated with the current flow") + } + } else { + // every other state must be a sub-flow entry state + if !subFlowEntryStateAllowed { + return fmt.Errorf("next state with index %d is not a sub-flow entry state", index) + } + } + + // append the current nextState to the list of scheduled states + scheduledStates = append(scheduledStates, nextState) + } + + // get the current sub-flow stack from the stash + stack := aec.stash.Get("_.scheduled_states").Array() + + newStack := make([]StateName, len(stack)) + + for index := range newStack { + newStack[index] = StateName(stack[index].String()) + } + + // prepend the states to the list of previously defined scheduled states + newStack = append(scheduledStates, newStack...) + + err = aec.stash.Set("_.scheduled_states", newStack) + if err != nil { + return fmt.Errorf("failed to stash scheduled states while staring a sub-flow: %w", err) + } + + return aec.continueFlow(entryState, nil, false) } -// ContinueFlowWithError continues the Flow execution to the specified nextState with an error type. -func (mec *defaultActionExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { - return mec.continueFlow(nextState, flowErr) +// EndSubFlow ends the current sub-flow and progresses the flow to the previously defined nextStates (see StartSubFlow) +func (aec *defaultActionExecutionContext) EndSubFlow() error { + // retrieve the previously scheduled states form the stash + stack := aec.stash.Get("_.scheduled_states").Array() + + newStack := make([]StateName, len(stack)) + + for index := range newStack { + newStack[index] = StateName(stack[index].String()) + } + + // if there is no scheduled state left, continue to the end state + if len(newStack) == 0 { + newStack = append(newStack, aec.GetEndState()) + } + + // get and remove first stack item + nextState := newStack[0] + newStack = newStack[1:] + + // stash the updated list of scheduled states + if err := aec.stash.Set("_.scheduled_states", newStack); err != nil { + return fmt.Errorf("failed to stash scheduled states while ending the sub-flow: %w", err) + } + + return aec.continueFlow(nextState, nil, true) } diff --git a/backend/flowpilot/context_action_init.go b/backend/flowpilot/context_action_init.go index d17095c8f..4321c09fd 100644 --- a/backend/flowpilot/context_action_init.go +++ b/backend/flowpilot/context_action_init.go @@ -1,25 +1,27 @@ package flowpilot -import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" +import ( + "github.com/teamhanko/hanko/backend/flowpilot/utils" +) // defaultActionInitializationContext is the default implementation of the actionInitializationContext interface. type defaultActionInitializationContext struct { - schema InitializationSchema // InitializationSchema for action initialization. - isSuspended bool // Flag indicating if the method is suspended. - stash jsonmanager.ReadOnlyJSONManager // ReadOnlyJSONManager for accessing stash data. + schema InitializationSchema // InitializationSchema for action initialization. + isSuspended bool // Flag indicating if the method is suspended. + stash utils.Stash // ReadOnlyJSONManager for accessing stash data. } -// AddInputs adds input data to the InitializationSchema and returns a defaultSchema instance. -func (mic *defaultActionInitializationContext) AddInputs(inputList ...Input) { - mic.schema.AddInputs(inputList...) +// AddInputs adds input data to the InitializationSchema. +func (aic *defaultActionInitializationContext) AddInputs(inputs ...Input) { + aic.schema.AddInputs(inputs...) } // SuspendAction sets the isSuspended flag to indicate the action is suspended. -func (mic *defaultActionInitializationContext) SuspendAction() { - mic.isSuspended = true +func (aic *defaultActionInitializationContext) SuspendAction() { + aic.isSuspended = true } // Stash returns the ReadOnlyJSONManager for accessing stash data. -func (mic *defaultActionInitializationContext) Stash() jsonmanager.ReadOnlyJSONManager { - return mic.stash +func (aic *defaultActionInitializationContext) Stash() utils.Stash { + return aic.stash } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index 9bab0085e..a6ff516af 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -3,39 +3,39 @@ package flowpilot import ( "fmt" "github.com/gofrs/uuid" - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // defaultFlowContext is the default implementation of the flowContext interface. type defaultFlowContext struct { - payload jsonmanager.JSONManager // JSONManager for payload data. - stash jsonmanager.JSONManager // JSONManager for stash data. - flow Flow // The associated Flow instance. - dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. - flowModel FlowModel // The current FlowModel. + payload utils.Payload // JSONManager for payload data. + stash utils.Stash // JSONManager for stash data. + flow defaultFlow // The associated defaultFlow instance. + dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. + flowModel FlowModel // The current FlowModel. } -// GetFlowID returns the unique ID of the current Flow. +// GetFlowID returns the unique ID of the current defaultFlow. func (fc *defaultFlowContext) GetFlowID() uuid.UUID { return fc.flowModel.ID } -// GetPath returns the current path within the Flow. +// GetPath returns the current path within the defaultFlow. func (fc *defaultFlowContext) GetPath() string { - return fc.flow.Path + return fc.flow.path } -// GetInitialState returns the initial state of the Flow. +// GetInitialState returns the initial addState of the defaultFlow. func (fc *defaultFlowContext) GetInitialState() StateName { - return fc.flow.InitialState + return fc.flow.initialState } -// GetCurrentState returns the current state of the Flow. +// GetCurrentState returns the current addState of the defaultFlow. func (fc *defaultFlowContext) GetCurrentState() StateName { return fc.flowModel.CurrentState } -// CurrentStateEquals returns true, when one of the given stateNames matches the current state name. +// CurrentStateEquals returns true, when one of the given stateNames matches the current addState name. func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { for _, s := range stateNames { if s == fc.flowModel.CurrentState { @@ -46,33 +46,34 @@ func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { return false } -// GetPreviousState returns a pointer to the previous state of the Flow. +// GetPreviousState returns a pointer to the previous addState of the flow. func (fc *defaultFlowContext) GetPreviousState() *StateName { - return fc.flowModel.PreviousState + // TODO: A new state history logic needs to be implemented to reintroduce the functionality + return nil } -// GetErrorState returns the designated error state of the Flow. +// GetErrorState returns the designated error addState of the flow. func (fc *defaultFlowContext) GetErrorState() StateName { - return fc.flow.ErrorState + return fc.flow.errorState } -// GetEndState returns the final state of the Flow. +// GetEndState returns the final addState of the flow. func (fc *defaultFlowContext) GetEndState() StateName { - return fc.flow.EndState + return fc.flow.endState } // Stash returns the JSONManager for accessing stash data. -func (fc *defaultFlowContext) Stash() jsonmanager.JSONManager { +func (fc *defaultFlowContext) Stash() utils.Stash { return fc.stash } -// StateExists checks if a given state exists within the Flow. +// StateExists checks if a given addState exists within the flow. func (fc *defaultFlowContext) StateExists(stateName StateName) bool { return fc.flow.stateExists(stateName) } -// FetchMethodInput fetches input data for a specific method. -func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (jsonmanager.ReadOnlyJSONManager, error) { +// FetchActionInput fetches input data for a specific action. +func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (utils.ReadOnlyActionInput, error) { // Find the last Transition with the specified method from the database wrapper. t, err := fc.dbw.FindLastTransitionWithAction(fc.flowModel.ID, methodName) if err != nil { @@ -81,19 +82,14 @@ func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (jsonmanag // If no Transition is found, return an empty JSONManager. if t == nil { - return jsonmanager.NewJSONManager(), nil + return utils.NewActionInput(), nil } // Parse input data from the Transition. - inputData, err := jsonmanager.NewJSONManagerFromString(t.InputData) + inputData, err := utils.NewActionInputFromString(t.InputData) if err != nil { return nil, fmt.Errorf("failed to decode Transition data: %w", err) } return inputData, nil } - -// getCurrentTransitions retrieves the Transitions for the current state. -func (fc *defaultFlowContext) getCurrentTransitions() *Transitions { - return fc.flow.getTransitionsForState(fc.flowModel.CurrentState) -} diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go index dedc88fc8..54a1439dc 100644 --- a/backend/flowpilot/db.go +++ b/backend/flowpilot/db.go @@ -6,33 +6,32 @@ import ( "time" ) -// FlowModel represents the database model for a Flow. +// FlowModel represents the database model for a defaultFlow. type FlowModel struct { - ID uuid.UUID // Unique ID of the Flow. - CurrentState StateName // Current state of the Flow. - PreviousState *StateName // Previous state of the Flow. - StashData string // Stash data associated with the Flow. - Version int // Version of the Flow. - Completed bool // Flag indicating if the Flow is completed. - ExpiresAt time.Time // Expiry time of the Flow. - CreatedAt time.Time // Creation time of the Flow. - UpdatedAt time.Time // Update time of the Flow. + ID uuid.UUID // Unique ID of the defaultFlow. + CurrentState StateName // Current addState of the defaultFlow. + StashData string // Stash data associated with the defaultFlow. + Version int // Version of the defaultFlow. + Completed bool // Flag indicating if the defaultFlow is completed. + ExpiresAt time.Time // Expiry time of the defaultFlow. + CreatedAt time.Time // Creation time of the defaultFlow. + UpdatedAt time.Time // Update time of the defaultFlow. } // TransitionModel represents the database model for a Transition. type TransitionModel struct { ID uuid.UUID // Unique ID of the Transition. - FlowID uuid.UUID // ID of the associated Flow. + FlowID uuid.UUID // ID of the associated defaultFlow. Action ActionName // Name of the action associated with the Action. - FromState StateName // Source state of the Transition. - ToState StateName // Target state of the Transition. + FromState StateName // Source addState of the Transition. + ToState StateName // Target addState of the Transition. InputData string // Input data associated with the Transition. ErrorCode *string // Optional error code associated with the Transition. CreatedAt time.Time // Creation time of the Transition. UpdatedAt time.Time // Update time of the Transition. } -// FlowDB is the interface for interacting with the Flow database. +// FlowDB is the interface for interacting with the defaultFlow database. type FlowDB interface { GetFlow(flowID uuid.UUID) (*FlowModel, error) CreateFlow(flowModel FlowModel) error @@ -62,73 +61,70 @@ func wrapDB(db FlowDB) FlowDBWrapper { return &DefaultFlowDBWrapper{FlowDB: db} } -// flowCreationParam holds parameters for creating a new Flow. +// flowCreationParam holds parameters for creating a new defaultFlow. type flowCreationParam struct { - currentState StateName // Initial state of the Flow. - expiresAt time.Time // Expiry time of the Flow. + currentState StateName // Initial addState of the defaultFlow. + expiresAt time.Time // Expiry time of the defaultFlow. } -// CreateFlowWithParam creates a new Flow with the given parameters. +// CreateFlowWithParam creates a new defaultFlow with the given parameters. func (w *DefaultFlowDBWrapper) CreateFlowWithParam(p flowCreationParam) (*FlowModel, error) { - // Generate a new UUID for the Flow. + // Generate a new UUID for the defaultFlow. flowID, err := uuid.NewV4() if err != nil { - return nil, fmt.Errorf("failed to generate a new Flow id: %w", err) + return nil, fmt.Errorf("failed to generate a new defaultFlow id: %w", err) } // Prepare the FlowModel for creation. fm := FlowModel{ - ID: flowID, - CurrentState: p.currentState, - PreviousState: nil, - StashData: "{}", - Version: 0, - Completed: false, - ExpiresAt: p.expiresAt, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), + ID: flowID, + CurrentState: p.currentState, + StashData: "{}", + Version: 0, + Completed: false, + ExpiresAt: p.expiresAt, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), } - // Create the Flow in the database. + // Create the defaultFlow in the database. err = w.CreateFlow(fm) if err != nil { - return nil, fmt.Errorf("failed to store a new Flow to the dbw: %w", err) + return nil, fmt.Errorf("failed to store a new defaultFlow to the dbw: %w", err) } return &fm, nil } -// flowUpdateParam holds parameters for updating a Flow. +// flowUpdateParam holds parameters for updating a defaultFlow. type flowUpdateParam struct { - flowID uuid.UUID // ID of the Flow to update. - nextState StateName // Next state of the Flow. - previousState *StateName // Previous state of the Flow. - stashData string // Updated stash data for the Flow. - version int // Updated version of the Flow. - completed bool // Flag indicating if the Flow is completed. - expiresAt time.Time // Updated expiry time of the Flow. - createdAt time.Time // Original creation time of the Flow. + flowID uuid.UUID // ID of the flow to update. + nextState StateName // Next addState of the flow. + stashData string // Updated stash data for the flow. + version int // Updated version of the flow. + completed bool // Flag indicating if the flow is completed. + expiresAt time.Time // Updated expiry time of the flow. + createdAt time.Time // Original creation time of the flow. } -// UpdateFlowWithParam updates the specified Flow with the given parameters. +// UpdateFlowWithParam updates the specified defaultFlow with the given parameters. func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowModel, error) { // Prepare the updated FlowModel. fm := FlowModel{ - ID: p.flowID, - CurrentState: p.nextState, - PreviousState: p.previousState, - StashData: p.stashData, - Version: p.version, - Completed: p.completed, - ExpiresAt: p.expiresAt, - UpdatedAt: time.Now().UTC(), - CreatedAt: p.createdAt, + ID: p.flowID, + CurrentState: p.nextState, + StashData: p.stashData, + Version: p.version, + Completed: p.completed, + ExpiresAt: p.expiresAt, + UpdatedAt: time.Now().UTC(), + CreatedAt: p.createdAt, } - // Update the Flow in the database. + // Update the defaultFlow in the database. err := w.UpdateFlow(fm) if err != nil { - return nil, fmt.Errorf("failed to store updated Flow to the dbw: %w", err) + return nil, fmt.Errorf("failed to store updated flow to the dbw: %w", err) } return &fm, nil @@ -136,10 +132,10 @@ func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowMode // transitionCreationParam holds parameters for creating a new Transition. type transitionCreationParam struct { - flowID uuid.UUID // ID of the associated Flow. + flowID uuid.UUID // ID of the associated defaultFlow. actionName ActionName // Name of the action associated with the Transition. - fromState StateName // Source state of the Transition. - toState StateName // Target state of the Transition. + fromState StateName // Source addState of the Transition. + toState StateName // Target addState of the Transition. inputData string // Input data associated with the Transition. flowError FlowError // Optional flowError associated with the Transition. } diff --git a/backend/flowpilot/errors.go b/backend/flowpilot/errors.go index c018bea0d..e6703365f 100644 --- a/backend/flowpilot/errors.go +++ b/backend/flowpilot/errors.go @@ -33,7 +33,7 @@ type InputError interface { // defaultError is a base struct for custom error types. type defaultError struct { - origin error // The error origin. + cause error // The error cause. code string // Unique error code. message string // Contains a description of the error. errorText string // The string representation of the error. @@ -51,7 +51,7 @@ func (e *defaultError) Message() string { // Unwrap returns the wrapped error. func (e *defaultError) Unwrap() error { - return e.origin + return e.cause } // Error returns the formatted error message. @@ -61,17 +61,17 @@ func (e *defaultError) Error() string { // toPublicError converts the error to a PublicError for public exposure. func (e *defaultError) toPublicError(debug bool) PublicError { - pe := PublicError{ + publicError := PublicError{ Code: e.Code(), Message: e.Message(), } - if debug && e.origin != nil { - str := e.origin.Error() - pe.Origin = &str + if debug && e.cause != nil { + cause := e.cause.Error() + publicError.Cause = &cause } - return pe + return publicError } // defaultFlowError is a struct for flow-related errors. @@ -81,26 +81,31 @@ type defaultFlowError struct { status int // The suggested HTTP status code. } -func createErrorText(code, message string, origin error) string { - txt := fmt.Sprintf("%s - %s", code, message) - if origin != nil { - txt = fmt.Sprintf("%s: %s", txt, origin.Error()) +// createErrorText creates the text used as the string representation of the error. +func createErrorText(code, message string, cause error) string { + text := fmt.Sprintf("%s - %s", code, message) + + if cause != nil { + text = fmt.Sprintf("%s: %s", text, cause.Error()) } - return txt + + return text } // NewFlowError creates a new FlowError instance. func NewFlowError(code, message string, status int) FlowError { - return newFlowErrorWithOrigin(code, message, status, nil) + return newFlowErrorWithCause(code, message, status, nil) } -// newFlowErrorWithOrigin creates a new FlowError instance with an origin error. -func newFlowErrorWithOrigin(code, message string, status int, origin error) FlowError { +// newFlowErrorWithCause creates a new FlowError instance with an error cause. +func newFlowErrorWithCause(code, message string, status int, cause error) FlowError { + errorText := createErrorText(code, message, cause) + e := defaultError{ - origin: origin, + cause: cause, code: code, message: message, - errorText: createErrorText(code, message, origin), + errorText: errorText, } return &defaultFlowError{defaultError: e, status: status} @@ -113,7 +118,7 @@ func (e *defaultFlowError) Status() int { // Wrap wraps the error with another error. func (e *defaultFlowError) Wrap(err error) FlowError { - return newFlowErrorWithOrigin(e.code, e.message, e.status, err) + return newFlowErrorWithCause(e.code, e.message, e.status, err) } // defaultInputError is a struct for input-related errors. @@ -123,16 +128,18 @@ type defaultInputError struct { // NewInputError creates a new InputError instance. func NewInputError(code, message string) InputError { - return newInputErrorWithOrigin(code, message, nil) + return newInputErrorWithCause(code, message, nil) } -// newInputErrorWithOrigin creates a new InputError instance with an origin error. -func newInputErrorWithOrigin(code, message string, origin error) InputError { +// newInputErrorWithCause creates a new InputError instance with an error cause. +func newInputErrorWithCause(code, message string, cause error) InputError { + errorText := createErrorText(code, message, cause) + e := defaultError{ - origin: origin, + cause: cause, code: code, message: message, - errorText: createErrorText(code, message, origin), + errorText: errorText, } return &defaultInputError{defaultError: e} @@ -140,7 +147,7 @@ func newInputErrorWithOrigin(code, message string, origin error) InputError { // Wrap wraps the error with another error. func (e *defaultInputError) Wrap(err error) InputError { - return newInputErrorWithOrigin(e.code, e.message, err) + return newInputErrorWithCause(e.code, e.message, err) } // Predefined flow error types diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index 4c0fa3d4a..ff5c246e0 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -20,7 +20,7 @@ func (i InputData) getJSONStringOrDefault() string { return i.JSONString } -// flowExecutionOptions represents options for executing a Flow. +// flowExecutionOptions represents options for executing a defaultFlow. type flowExecutionOptions struct { action string inputData InputData @@ -40,10 +40,10 @@ func WithInputData(inputData InputData) func(*flowExecutionOptions) { } } -// StateName represents the name of a state in a Flow. +// StateName represents the name of a state in a defaultFlow. type StateName string -// ActionName represents the name of a action associated with a Transition. +// ActionName represents the name of an action associated with a Transition. type ActionName string // TODO: Should it be possible to partially implement the Action interface? E.g. when a action does not require initialization. @@ -56,124 +56,197 @@ type Action interface { Execute(ExecutionContext) error // Execute the action. } +type Actions []Action + // Transition holds an action associated with a state transition. type Transition struct { Action Action } +func (a Actions) getByName(name ActionName) (Action, error) { + for _, action := range a { + currentName := action.GetName() + + if currentName == name { + return action, nil + } + } + + return nil, fmt.Errorf("action '%s' not found", name) +} + // Transitions is a collection of Transition instances. type Transitions []Transition -// getAction returns the Action associated with the specified name. -func (ts *Transitions) getAction(actionName ActionName) (Action, error) { +// getActions returns the Action associated with the specified name. +func (ts *Transitions) getActions() []Action { + var actions []Action + for _, t := range *ts { - if t.Action.GetName() == actionName { - return t.Action, nil - } + actions = append(actions, t.Action) } - return nil, errors.New(fmt.Sprintf("action '%s' not valid", actionName)) + return actions } +type stateDetail struct { + flow StateTransitions + subFlows SubFlows + actions Actions +} + +// stateDetails maps states to associated Actions, flows and sub-flows. +type stateDetails map[StateName]stateDetail + // StateTransitions maps states to associated Transitions. type StateTransitions map[StateName]Transitions -// Flow defines a flow structure with states, transitions, and settings. -type Flow struct { - Flow StateTransitions // State transitions mapping. - Path string // Flow path or identifier. - InitialState StateName // Initial state of the flow. - ErrorState StateName // State representing errors. - EndState StateName // Final state of the flow. - TTL time.Duration // Time-to-live for the flow. - Debug bool // Enables debug mode. +// SubFlows maps a sub-flow init state to StateTransitions. +type SubFlows map[StateName]SubFlow + +func (sfs SubFlows) isEntryStateAllowed(entryState StateName) bool { + for _, subFlow := range sfs { + subFlowInitState := subFlow.getInitialState() + + if subFlowInitState == entryState { + return true + } + } + + return false } -// stateExists checks if a state exists in the Flow. -func (f *Flow) stateExists(stateName StateName) bool { - _, ok := f.Flow[stateName] +type flow interface { + stateExists(stateName StateName) bool + getStateDetail(stateName StateName) (*stateDetail, error) + getSubFlows() SubFlows + getFlow() StateTransitions +} + +type Flow interface { + Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) + ResultFromError(err error) FlowResult + setDefaults() + validate() error + flow +} + +type SubFlow interface { + getInitialState() StateName + flow +} + +// defaultFlow defines a flow structure with states, transitions, and settings. +type defaultFlow struct { + flow StateTransitions // State transitions mapping. + subFlows SubFlows // TODO + stateDetails stateDetails // + path string // flow path or identifier. + initialState StateName // Initial state of the flow. + errorState StateName // State representing errors. + endState StateName // Final state of the flow. + ttl time.Duration // Time-to-live for the flow. + debug bool // Enables debug mode. +} + +// stateExists checks if a state exists in the defaultFlow. +func (f *defaultFlow) stateExists(stateName StateName) bool { + _, ok := f.flow[stateName] return ok } -// getTransitionsForState returns transitions for a specified state. -func (f *Flow) getTransitionsForState(stateName StateName) *Transitions { - if ts, ok := f.Flow[stateName]; ok && len(ts) > 0 { - return &ts +// getActionsForState returns transitions for a specified state. +func (f *defaultFlow) getStateDetail(stateName StateName) (*stateDetail, error) { + if detail, ok := f.stateDetails[stateName]; ok { + return &detail, nil } - return nil + + return nil, fmt.Errorf("unknown state: %s", stateName) } -// setDefaults sets default values for Flow settings. -func (f *Flow) setDefaults() { - if f.TTL.Seconds() == 0 { - f.TTL = time.Minute * 60 +func (f *defaultFlow) getInitialState() StateName { + return f.initialState +} + +func (f *defaultFlow) getSubFlows() SubFlows { + return f.subFlows +} + +func (f *defaultFlow) getFlow() StateTransitions { + return f.flow +} + +// setDefaults sets default values for defaultFlow settings. +func (f *defaultFlow) setDefaults() { + if f.ttl.Seconds() == 0 { + f.ttl = time.Minute * 60 } } -// validate performs validation checks on the Flow configuration. -func (f *Flow) validate() error { - // Validate fixed states and their presence in the Flow. - if len(f.InitialState) == 0 { - return errors.New("fixed state 'InitialState' is not set") +// TODO: validate while building the flow +// validate performs validation checks on the defaultFlow configuration. +func (f *defaultFlow) validate() error { + // Validate fixed states and their presence in the flow. + if len(f.initialState) == 0 { + return errors.New("fixed state 'initialState' is not set") } - if len(f.ErrorState) == 0 { - return errors.New("fixed state 'ErrorState' is not set") + if len(f.errorState) == 0 { + return errors.New("fixed state 'errorState' is not set") } - if len(f.EndState) == 0 { - return errors.New("fixed state 'EndState' is not set") + if len(f.endState) == 0 { + return errors.New("fixed state 'endState' is not set") } - if !f.stateExists(f.InitialState) { - return errors.New("fixed state 'InitialState' does not belong to the flow") + if !f.stateExists(f.initialState) { + return errors.New("fixed state 'initialState' does not belong to the flow") } - if !f.stateExists(f.ErrorState) { - return errors.New("fixed state 'ErrorState' does not belong to the flow") + if !f.stateExists(f.errorState) { + return errors.New("fixed state 'errorState' does not belong to the flow") } - if !f.stateExists(f.EndState) { - return errors.New("fixed state 'EndState' does not belong to the flow") + if !f.stateExists(f.endState) { + return errors.New("fixed state 'endState' does not belong to the flow") } - if ts := f.getTransitionsForState(f.EndState); ts != nil { - return fmt.Errorf("the specified EndState '%s' is not allowed to have transitions", f.EndState) + if detail, _ := f.getStateDetail(f.endState); detail == nil || len(detail.actions) > 0 { + return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", f.endState) } - // TODO: Additional validation for unique State and Action names,... - return nil } -// Execute handles the execution of actions for a Flow. -func (f *Flow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) { +// Execute handles the execution of actions for a defaultFlow. +func (f *defaultFlow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) { // Process execution options. var executionOptions flowExecutionOptions + for _, option := range opts { option(&executionOptions) } - // Set default values for Flow settings. + // Set default values for flow settings. f.setDefaults() - // Perform validation checks on the Flow configuration. + // Perform validation checks on the flow configuration. if err := f.validate(); err != nil { return nil, fmt.Errorf("invalid flow: %w", err) } if len(executionOptions.action) == 0 { - // If the action is empty, create a new Flow. + // If the action is empty, create a new flow. return createAndInitializeFlow(db, *f) } - // Otherwise, update an existing Flow. + // Otherwise, update an existing flow. return executeFlowAction(db, *f, executionOptions) } -// ResultFromError returns an error response for the Flow. -func (f *Flow) ResultFromError(err error) (result FlowResult) { +// ResultFromError returns an error response for the defaultFlow. +func (f *defaultFlow) ResultFromError(err error) (result FlowResult) { flowError := ErrorTechnical - if err2, ok := err.(FlowError); ok { - flowError = err2 + if e, ok := err.(FlowError); ok { + flowError = e } else { flowError = flowError.Wrap(err) } - return newFlowResultFromError(f.ErrorState, flowError, f.Debug) + return newFlowResultFromError(f.errorState, flowError, f.debug) } diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go index 97344e7e6..a6bac44af 100644 --- a/backend/flowpilot/input.go +++ b/backend/flowpilot/input.go @@ -1,7 +1,7 @@ package flowpilot import ( - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" "regexp" ) @@ -34,7 +34,7 @@ type Input interface { shouldPersist() bool shouldPreserve() bool isIncludedOnState(stateName StateName) bool - validate(stateName StateName, inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.JSONManager) bool + validate(stateName StateName, inputData utils.ReadOnlyActionInput, stashData utils.Stash) bool toPublicInput() *PublicInput } @@ -61,16 +61,18 @@ type DefaultInput struct { } // newInput creates a new DefaultInput instance with provided parameters. -func newInput(name string, t InputType, persistValue bool) Input { +func newInput(name string, inputType InputType, persistValue bool) Input { + extraOptions := defaultExtraInputOptions{ + preserveValue: false, + persistValue: persistValue, + includeOnStates: []StateName{}, + compareWithStash: false, + } + return &DefaultInput{ - name: name, - dataType: t, - defaultExtraInputOptions: defaultExtraInputOptions{ - preserveValue: false, - persistValue: persistValue, - includeOnStates: []StateName{}, - compareWithStash: false, - }, + name: name, + dataType: inputType, + defaultExtraInputOptions: extraOptions, } } @@ -190,7 +192,7 @@ func (i *DefaultInput) shouldPreserve() bool { } // validate performs validation on the input field. -func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadOnlyJSONManager, stashData jsonmanager.JSONManager) bool { +func (i *DefaultInput) validate(stateName StateName, inputData utils.ReadOnlyActionInput, stashData utils.Stash) bool { // TODO: Replace with more structured validation logic. var inputValue *string @@ -251,21 +253,21 @@ func (i *DefaultInput) validate(stateName StateName, inputData jsonmanager.ReadO // toPublicInput converts the DefaultInput to a PublicInput for public exposure. func (i *DefaultInput) toPublicInput() *PublicInput { - var pe *PublicError + var publicError *PublicError if i.error != nil { e := i.error.toPublicError(true) - pe = &e + publicError = &e } return &PublicInput{ - Name: i.name, - Type: i.dataType, - Value: i.value, - MinLength: i.minLength, - MaxLength: i.maxLength, - Required: i.required, - Hidden: i.hidden, - Error: pe, + Name: i.name, + Type: i.dataType, + Value: i.value, + MinLength: i.minLength, + MaxLength: i.maxLength, + Required: i.required, + Hidden: i.hidden, + PublicError: publicError, } } diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index a3003a6ad..43e0acd0f 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -1,7 +1,7 @@ package flowpilot import ( - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + "github.com/teamhanko/hanko/backend/flowpilot/utils" "github.com/tidwall/gjson" ) @@ -17,9 +17,9 @@ type ExecutionSchema interface { SetError(inputName string, inputError InputError) getInput(name string) Input - getOutputData() jsonmanager.ReadOnlyJSONManager - getDataToPersist() jsonmanager.ReadOnlyJSONManager - validateInputData(stateName StateName, stash jsonmanager.JSONManager) bool + getOutputData() utils.ReadOnlyActionInput + getDataToPersist() utils.ReadOnlyActionInput + validateInputData(stateName StateName, stash utils.Stash) bool toInitializationSchema() InitializationSchema toPublicSchema(stateName StateName) PublicSchema } @@ -33,13 +33,14 @@ type PublicSchema []*PublicInput // defaultSchema implements the InitializationSchema interface and holds a collection of input fields. type defaultSchema struct { inputs - inputData jsonmanager.ReadOnlyJSONManager - outputData jsonmanager.JSONManager + inputData utils.ReadOnlyActionInput + outputData utils.ActionInput } // newSchemaWithInputData creates a new ExecutionSchema with input data. -func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) ExecutionSchema { - outputData := jsonmanager.NewJSONManager() +func newSchemaWithInputData(inputData utils.ActionInput) ExecutionSchema { + outputData := utils.NewActionInput() + return &defaultSchema{ inputData: inputData, outputData: outputData, @@ -47,8 +48,8 @@ func newSchemaWithInputData(inputData jsonmanager.ReadOnlyJSONManager) Execution } // newSchemaWithInputData creates a new ExecutionSchema with input data. -func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (ExecutionSchema, error) { - data, err := jsonmanager.NewJSONManagerFromString(outputData.String()) +func newSchemaWithOutputData(outputData utils.ReadOnlyActionInput) (ExecutionSchema, error) { + data, err := utils.NewActionInputFromString(outputData.String()) if err != nil { return nil, err } @@ -61,7 +62,7 @@ func newSchemaWithOutputData(outputData jsonmanager.ReadOnlyJSONManager) (Execut // newSchema creates a new ExecutionSchema with no input data. func newSchema() ExecutionSchema { - inputData := jsonmanager.NewJSONManager() + inputData := utils.NewActionInput() return newSchemaWithInputData(inputData) } @@ -82,16 +83,16 @@ func (s *defaultSchema) Set(path string, value interface{}) error { // AddInputs adds input fields to the defaultSchema and returns the updated schema. func (s *defaultSchema) AddInputs(inputList ...Input) { - for _, i := range inputList { - s.inputs = append(s.inputs, i) + for _, input := range inputList { + s.inputs = append(s.inputs, input) } } // getInput retrieves an input field from the schema based on its name. func (s *defaultSchema) getInput(name string) Input { - for _, i := range s.inputs { - if i.getName() == name { - return i + for _, input := range s.inputs { + if input.getName() == name { + return input } } @@ -100,17 +101,17 @@ func (s *defaultSchema) getInput(name string) Input { // SetError sets an error for an input field in the schema. func (s *defaultSchema) SetError(inputName string, inputError InputError) { - if i := s.getInput(inputName); i != nil { - i.setError(inputError) + if input := s.getInput(inputName); input != nil { + input.setError(inputError) } } // validateInputData validates the input data based on the input definitions in the schema. -func (s *defaultSchema) validateInputData(stateName StateName, stash jsonmanager.JSONManager) bool { +func (s *defaultSchema) validateInputData(stateName StateName, stash utils.Stash) bool { valid := true - for _, i := range s.inputs { - if !i.validate(stateName, s.inputData, stash) && valid { + for _, input := range s.inputs { + if !input.validate(stateName, s.inputData, stash) && valid { valid = false } } @@ -119,12 +120,12 @@ func (s *defaultSchema) validateInputData(stateName StateName, stash jsonmanager } // getDataToPersist filters and returns data that should be persisted based on schema definitions. -func (s *defaultSchema) getDataToPersist() jsonmanager.ReadOnlyJSONManager { - toPersist := jsonmanager.NewJSONManager() +func (s *defaultSchema) getDataToPersist() utils.ReadOnlyActionInput { + toPersist := utils.NewActionInput() - for _, i := range s.inputs { - if v := s.inputData.Get(i.getName()); v.Exists() && i.shouldPersist() { - _ = toPersist.Set(i.getName(), v.Value()) + for _, input := range s.inputs { + if v := s.inputData.Get(input.getName()); v.Exists() && input.shouldPersist() { + _ = toPersist.Set(input.getName(), v.Value()) } } @@ -132,32 +133,32 @@ func (s *defaultSchema) getDataToPersist() jsonmanager.ReadOnlyJSONManager { } // getOutputData returns the output data from the schema. -func (s *defaultSchema) getOutputData() jsonmanager.ReadOnlyJSONManager { +func (s *defaultSchema) getOutputData() utils.ReadOnlyActionInput { return s.outputData } // toPublicSchema converts defaultSchema to PublicSchema for public exposure. func (s *defaultSchema) toPublicSchema(stateName StateName) PublicSchema { - var pi PublicSchema + var publicSchema PublicSchema - for _, i := range s.inputs { - if !i.isIncludedOnState(stateName) { + for _, input := range s.inputs { + if !input.isIncludedOnState(stateName) { continue } - outputValue := s.outputData.Get(i.getName()) - inputValue := s.inputData.Get(i.getName()) + outputValue := s.outputData.Get(input.getName()) + inputValue := s.inputData.Get(input.getName()) if outputValue.Exists() { - i.setValue(outputValue.Value()) + input.setValue(outputValue.Value()) } - if i.shouldPreserve() && inputValue.Exists() && !outputValue.Exists() { - i.setValue(inputValue.Value()) + if input.shouldPreserve() && inputValue.Exists() && !outputValue.Exists() { + input.setValue(inputValue.Value()) } - pi = append(pi, i.toPublicInput()) + publicSchema = append(publicSchema, input.toPublicInput()) } - return pi + return publicSchema } diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index d14a0f17a..2faf51f63 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -8,10 +8,10 @@ import ( // PublicAction represents a link to an action. type PublicAction struct { - Href string `json:"href"` - Inputs PublicSchema `json:"inputs"` - ActionName ActionName `json:"action_name"` - Description string `json:"description"` + Href string `json:"href"` + PublicSchema PublicSchema `json:"schema"` + Name ActionName `json:"action"` + Description string `json:"description"` } // PublicActions is a collection of PublicAction instances. @@ -26,28 +26,28 @@ func (ls *PublicActions) Add(l PublicAction) { type PublicError struct { Code string `json:"code"` Message string `json:"message"` - Origin *string `json:"origin,omitempty"` + Cause *string `json:"cause,omitempty"` } // PublicInput represents an input field for public exposure. type PublicInput struct { - Name string `json:"name"` - Type InputType `json:"type"` - Value interface{} `json:"value,omitempty"` - MinLength *int `json:"min_length,omitempty"` - MaxLength *int `json:"max_length,omitempty"` - Required *bool `json:"required,omitempty"` - Hidden *bool `json:"hidden,omitempty"` - Error *PublicError `json:"error,omitempty"` + Name string `json:"name"` + Type InputType `json:"type"` + Value interface{} `json:"value,omitempty"` + MinLength *int `json:"min_length,omitempty"` + MaxLength *int `json:"max_length,omitempty"` + Required *bool `json:"required,omitempty"` + Hidden *bool `json:"hidden,omitempty"` + PublicError *PublicError `json:"error,omitempty"` } // PublicResponse represents the response of an action execution. type PublicResponse struct { - State StateName `json:"state"` - Status int `json:"status"` - Payload interface{} `json:"payload,omitempty"` - Actions PublicActions `json:"actions"` - Error *PublicError `json:"error,omitempty"` + State StateName `json:"state"` + Status int `json:"status"` + Payload interface{} `json:"payload,omitempty"` + PublicActions PublicActions `json:"actions"` + PublicError *PublicError `json:"error,omitempty"` } // FlowResult interface defines methods for obtaining response and status. @@ -62,19 +62,22 @@ type DefaultFlowResult struct { } // newFlowResultFromResponse creates a FlowResult from a PublicResponse. -func newFlowResultFromResponse(response PublicResponse) FlowResult { - return DefaultFlowResult{PublicResponse: response} +func newFlowResultFromResponse(publicResponse PublicResponse) FlowResult { + return DefaultFlowResult{PublicResponse: publicResponse} } // newFlowResultFromError creates a FlowResult from a FlowError. func newFlowResultFromError(stateName StateName, flowError FlowError, debug bool) FlowResult { - pe := flowError.toPublicError(debug) + publicError := flowError.toPublicError(debug) + status := flowError.Status() - return DefaultFlowResult{PublicResponse: PublicResponse{ - State: stateName, - Status: flowError.Status(), - Error: &pe, - }} + publicResponse := PublicResponse{ + State: stateName, + Status: status, + PublicError: &publicError, + } + + return DefaultFlowResult{PublicResponse: publicResponse} } // Response returns the PublicResponse. @@ -102,25 +105,28 @@ type executionResult struct { } // generateResponse generates a response based on the execution result. -func (er *executionResult) generateResponse(fc defaultFlowContext) FlowResult { +func (er *executionResult) generateResponse(fc defaultFlowContext, debug bool) FlowResult { // Generate actions for the response. actions := er.generateActions(fc) + // Unmarshal the generated payload for the response. + payload := fc.payload.Unmarshal() + // Create the response object. resp := PublicResponse{ - State: er.nextState, - Status: http.StatusOK, - Payload: fc.payload.Unmarshal(), - Actions: actions, + State: er.nextState, + Status: http.StatusOK, + Payload: payload, + PublicActions: actions, } // Include flow error if present. if er.flowError != nil { status := er.flowError.Status() - publicError := er.flowError.toPublicError(false) + publicError := er.flowError.toPublicError(debug) resp.Status = status - resp.Error = &publicError + resp.PublicError = &publicError } return newFlowResultFromResponse(resp) @@ -128,63 +134,66 @@ func (er *executionResult) generateResponse(fc defaultFlowContext) FlowResult { // generateActions generates a collection of links based on the execution result. func (er *executionResult) generateActions(fc defaultFlowContext) PublicActions { - var actions PublicActions + var publicActions PublicActions - // Get transitions for the next state. - transitions := fc.flow.getTransitionsForState(er.nextState) + // Get actions for the next addState. + detail, _ := fc.flow.getStateDetail(er.nextState) - if transitions != nil { - for _, t := range *transitions { - currentActionName := t.Action.GetName() - currentDescription := t.Action.GetDescription() + if detail != nil { + for _, action := range detail.actions { + actionName := action.GetName() + actionDescription := action.GetDescription() // Create action HREF based on the current flow context and method name. - href := er.createHref(fc, currentActionName) - schema := er.getExecutionSchema(currentActionName) + href := er.createHref(fc, actionName) + schema := er.getExecutionSchema(actionName) if schema == nil { // Create schema if not available. - if schema = er.createSchema(fc, t.Action); schema == nil { + if schema = er.createSchema(fc, action); schema == nil { continue } } + publicSchema := schema.toPublicSchema(er.nextState) + // Create the action instance. - action := PublicAction{ - Href: href, - Inputs: schema.toPublicSchema(er.nextState), - ActionName: currentActionName, - Description: currentDescription, + publicAction := PublicAction{ + Href: href, + PublicSchema: publicSchema, + Name: actionName, + Description: actionDescription, } - actions.Add(action) + publicActions.Add(publicAction) } } - return actions + return publicActions } // createSchema creates an execution schema for a method if needed. -func (er *executionResult) createSchema(fc defaultFlowContext, method Action) ExecutionSchema { +func (er *executionResult) createSchema(fc defaultFlowContext, action Action) ExecutionSchema { var schema ExecutionSchema - var err error if er.actionExecutionResult != nil { + var err error + data := er.actionExecutionResult.schema.getOutputData() schema, err = newSchemaWithOutputData(data) + + if err != nil { + return nil + } } else { schema = newSchema() } - if err != nil { - return nil - } - - // Initialize the method. - mic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: fc.stash} - method.Initialize(&mic) + // Initialize the action. + aic := defaultActionInitializationContext{schema: schema.toInitializationSchema(), stash: fc.stash} + action.Initialize(&aic) - if mic.isSuspended { + if aic.isSuspended { return nil } @@ -192,8 +201,8 @@ func (er *executionResult) createSchema(fc defaultFlowContext, method Action) Ex } // getExecutionSchema gets the execution schema for a given method name. -func (er *executionResult) getExecutionSchema(methodName ActionName) ExecutionSchema { - if er.actionExecutionResult == nil || methodName != er.actionExecutionResult.actionName { +func (er *executionResult) getExecutionSchema(actionName ActionName) ExecutionSchema { + if er.actionExecutionResult == nil || actionName != er.actionExecutionResult.actionName { return nil } @@ -201,7 +210,7 @@ func (er *executionResult) getExecutionSchema(methodName ActionName) ExecutionSc } // createHref creates a link HREF based on the current flow context and method name. -func (er *executionResult) createHref(fc defaultFlowContext, methodName ActionName) string { - action := utils.CreateActionParam(string(methodName), fc.GetFlowID()) +func (er *executionResult) createHref(fc defaultFlowContext, actionName ActionName) string { + action := utils.CreateActionParam(string(actionName), fc.GetFlowID()) return fmt.Sprintf("%s?flowpilot_action=%s", fc.GetPath(), action) } diff --git a/backend/flowpilot/utils/storage.go b/backend/flowpilot/utils/storage.go new file mode 100644 index 000000000..a16438366 --- /dev/null +++ b/backend/flowpilot/utils/storage.go @@ -0,0 +1,50 @@ +package utils + +import ( + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" +) + +type Stash interface { + jsonmanager.JSONManager +} + +// NewStash creates a new instance of Stash with empty JSON data. +func NewStash() Stash { + return jsonmanager.NewJSONManager() +} + +// NewStashFromString creates a new instance of Stash with the given JSON data. +func NewStashFromString(data string) (Stash, error) { + return jsonmanager.NewJSONManagerFromString(data) +} + +type Payload interface { + jsonmanager.JSONManager +} + +// NewPayload creates a new instance of Payload with empty JSON data. +func NewPayload() Payload { + return jsonmanager.NewJSONManager() +} + +// NewPayloadFromString creates a new instance of Payload with the given JSON data. +func NewPayloadFromString(data string) (Payload, error) { + return jsonmanager.NewJSONManagerFromString(data) +} + +type ActionInput interface { + jsonmanager.JSONManager +} + +type ReadOnlyActionInput interface { + jsonmanager.ReadOnlyJSONManager +} + +// NewActionInput creates a new instance of ActionInput with empty JSON data. +func NewActionInput() ActionInput { + return jsonmanager.NewJSONManager() +} + +func NewActionInputFromString(data string) (ActionInput, error) { + return jsonmanager.NewJSONManagerFromString(data) +} diff --git a/backend/persistence/migrations/20230810173315_create_flows.up.fizz b/backend/persistence/migrations/20230810173315_create_flows.up.fizz index 82f2f3722..b3b9b9eff 100644 --- a/backend/persistence/migrations/20230810173315_create_flows.up.fizz +++ b/backend/persistence/migrations/20230810173315_create_flows.up.fizz @@ -1,7 +1,6 @@ create_table("flows") { t.Column("id", "uuid", {primary: true}) t.Column("current_state", "string") - t.Column("previous_state", "string", {"null": true}) t.Column("stash_data", "string") t.Column("version", "int") t.Column("completed", "bool", {"default": false}) diff --git a/backend/persistence/models/flow.go b/backend/persistence/models/flow.go index 7076ccac5..a5f50faba 100644 --- a/backend/persistence/models/flow.go +++ b/backend/persistence/models/flow.go @@ -11,15 +11,14 @@ import ( // Flow is used by pop to map your flows database table to your go code. type Flow struct { - ID uuid.UUID `json:"id" db:"id"` - CurrentState string `json:"current_state" db:"current_state"` - PreviousState *string `json:"previous_state" db:"previous_state"` - StashData string `json:"stash_data" db:"stash_data"` - Version int `json:"version" db:"version"` - Completed bool `json:"completed" db:"completed"` - ExpiresAt time.Time `json:"expires_at" db:"expires_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + ID uuid.UUID `json:"id" db:"id"` + CurrentState string `json:"current_state" db:"current_state"` + StashData string `json:"stash_data" db:"stash_data"` + Version int `json:"version" db:"version"` + Completed bool `json:"completed" db:"completed"` + ExpiresAt time.Time `json:"expires_at" db:"expires_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + CreatedAt time.Time `json:"created_at" db:"created_at"` // transitions transitions `json:"transitions" has_many:"transitions" order_by:"created_at desc"` } @@ -35,11 +34,6 @@ func (f *Flow) ToFlowpilotModel() *flowpilot.FlowModel { UpdatedAt: f.UpdatedAt, } - if f.PreviousState != nil { - ps := flowpilot.StateName(*f.PreviousState) - flow.PreviousState = &ps - } - return &flow } diff --git a/backend/persistence/models/flowdb.go b/backend/persistence/models/flowdb.go index 16929a3a5..d532fd5d6 100644 --- a/backend/persistence/models/flowdb.go +++ b/backend/persistence/models/flowdb.go @@ -87,15 +87,14 @@ func (flowDB FlowDB) GetFlow(flowID uuid.UUID) (*flowpilot.FlowModel, error) { func (flowDB FlowDB) CreateFlow(flowModel flowpilot.FlowModel) error { f := Flow{ - ID: flowModel.ID, - CurrentState: string(flowModel.CurrentState), - PreviousState: nil, - StashData: flowModel.StashData, - Version: flowModel.Version, - Completed: flowModel.Completed, - ExpiresAt: flowModel.ExpiresAt, - CreatedAt: flowModel.CreatedAt, - UpdatedAt: flowModel.UpdatedAt, + ID: flowModel.ID, + CurrentState: string(flowModel.CurrentState), + StashData: flowModel.StashData, + Version: flowModel.Version, + Completed: flowModel.Completed, + ExpiresAt: flowModel.ExpiresAt, + CreatedAt: flowModel.CreatedAt, + UpdatedAt: flowModel.UpdatedAt, } err := flowDB.tx.Create(&f) @@ -118,17 +117,12 @@ func (flowDB FlowDB) UpdateFlow(flowModel flowpilot.FlowModel) error { UpdatedAt: flowModel.UpdatedAt, } - if ps := flowModel.PreviousState; ps != nil { - previousState := string(*ps) - f.PreviousState = &previousState - } - previousVersion := flowModel.Version - 1 count, err := flowDB.tx. Where("id = ?", f.ID). Where("version = ?", previousVersion). - UpdateQuery(f, "current_state", "previous_state", "stash_data", "version", "completed") + UpdateQuery(f, "current_state", "stash_data", "version", "completed") if err != nil { return err } From d3c70ad7614aa994fc0d583c984fa2f6f44011f9 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Fri, 13 Oct 2023 17:43:05 +0200 Subject: [PATCH 011/278] feat(flowpilot): add a state history feature so you can go back to the previous state --- backend/flow_api_test/actions.go | 70 ++++++- backend/flow_api_test/flow.go | 20 +- backend/flow_api_test/types.go | 11 ++ backend/flowpilot/action_input.go | 20 ++ backend/flowpilot/builder.go | 14 +- backend/flowpilot/context.go | 21 +- backend/flowpilot/context_action_exec.go | 149 +++++++++----- backend/flowpilot/context_action_init.go | 8 +- backend/flowpilot/context_flow.go | 44 +++-- backend/flowpilot/flow.go | 31 +-- backend/flowpilot/input.go | 5 +- backend/flowpilot/input_schema.go | 29 ++- backend/flowpilot/payload.go | 17 ++ backend/flowpilot/response.go | 4 +- backend/flowpilot/stash.go | 182 ++++++++++++++++++ backend/flowpilot/utils/storage.go | 50 ----- .../20230810173315_create_flows.up.fizz | 2 +- 17 files changed, 495 insertions(+), 182 deletions(-) create mode 100644 backend/flowpilot/action_input.go create mode 100644 backend/flowpilot/payload.go create mode 100644 backend/flowpilot/stash.go delete mode 100644 backend/flowpilot/utils/storage.go diff --git a/backend/flow_api_test/actions.go b/backend/flow_api_test/actions.go index 2b5b529a9..2f1ef7f0c 100644 --- a/backend/flow_api_test/actions.go +++ b/backend/flow_api_test/actions.go @@ -6,6 +6,70 @@ import ( "github.com/teamhanko/hanko/backend/persistence/models" ) +type ContinueToFinal struct{} + +func (m ContinueToFinal) GetName() flowpilot.ActionName { + return ActionContinueToFinal +} + +func (m ContinueToFinal) GetDescription() string { + return "" +} + +func (m ContinueToFinal) Initialize(c flowpilot.InitializationContext) {} + +func (m ContinueToFinal) Execute(c flowpilot.ExecutionContext) error { + return c.ContinueFlow(StateSecondSubFlowFinal) +} + +type EndSubFlow struct{} + +func (m EndSubFlow) GetName() flowpilot.ActionName { + return ActionEndSubFlow +} + +func (m EndSubFlow) GetDescription() string { + return "" +} + +func (m EndSubFlow) Initialize(c flowpilot.InitializationContext) {} + +func (m EndSubFlow) Execute(c flowpilot.ExecutionContext) error { + return c.EndSubFlow() +} + +type StartSecondSubFlow struct{} + +func (m StartSecondSubFlow) GetName() flowpilot.ActionName { + return ActionStartSecondSubFlow +} + +func (m StartSecondSubFlow) GetDescription() string { + return "" +} + +func (m StartSecondSubFlow) Initialize(c flowpilot.InitializationContext) {} + +func (m StartSecondSubFlow) Execute(c flowpilot.ExecutionContext) error { + return c.StartSubFlow(StateSecondSubFlowInit) +} + +type StartFirstSubFlow struct{} + +func (m StartFirstSubFlow) GetName() flowpilot.ActionName { + return ActionStartFirstSubFlow +} + +func (m StartFirstSubFlow) GetDescription() string { + return "" +} + +func (m StartFirstSubFlow) Initialize(c flowpilot.InitializationContext) {} + +func (m StartFirstSubFlow) Execute(c flowpilot.ExecutionContext) error { + return c.StartSubFlow(StateFirstSubFlowInit, StateThirdSubFlowInit, StateSuccess) +} + type SubmitEmail struct{} func (m SubmitEmail) GetName() flowpilot.ActionName { @@ -312,9 +376,5 @@ func (m Back) GetDescription() string { func (m Back) Initialize(_ flowpilot.InitializationContext) {} func (m Back) Execute(c flowpilot.ExecutionContext) error { - if previousState := c.GetPreviousState(); previousState != nil { - return c.ContinueFlow(*previousState) - } - - return c.ContinueFlow(c.GetInitialState()) + return c.ContinueToPreviousState() } diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index 20aadb556..b56c0243a 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -5,8 +5,25 @@ import ( "time" ) +var ThirdSubFlow = flowpilot.NewSubFlow(). + State(StateThirdSubFlowInit, EndSubFlow{}, Back{}). + FixedStates(StateThirdSubFlowInit). + MustBuild() + +var SecondSubFlow = flowpilot.NewSubFlow(). + State(StateSecondSubFlowInit, ContinueToFinal{}, Back{}). + State(StateSecondSubFlowFinal, EndSubFlow{}, Back{}). + FixedStates(StateSecondSubFlowInit). + MustBuild() + +var FirstSubFlow = flowpilot.NewSubFlow(). + State(StateFirstSubFlowInit, StartSecondSubFlow{}, Back{}). + SubFlows(SecondSubFlow). + FixedStates(StateFirstSubFlowInit). + MustBuild() + var Flow = flowpilot.NewFlow("/flow_api_login"). - State(StateSignInOrSignUp, SubmitEmail{}, GetWAChallenge{}). + State(StateSignInOrSignUp, SubmitEmail{}, GetWAChallenge{}, StartFirstSubFlow{}, Back{}). State(StateLoginWithPasskey, VerifyWAPublicKey{}, Back{}). State(StateLoginWithPasscode, SubmitPasscodeCode{}, Back{}). State(StateLoginWithPasscode2FA, SubmitPasscodeCode{}). @@ -21,6 +38,7 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StateError). State(StateSuccess). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). + SubFlows(FirstSubFlow, ThirdSubFlow). TTL(time.Minute * 10). Debug(true). MustBuild() diff --git a/backend/flow_api_test/types.go b/backend/flow_api_test/types.go index f31818584..0626738c4 100644 --- a/backend/flow_api_test/types.go +++ b/backend/flow_api_test/types.go @@ -3,6 +3,11 @@ package flow_api_test import "github.com/teamhanko/hanko/backend/flowpilot" const ( + StateSecondSubFlowInit flowpilot.StateName = "StateSecondSubFlowInit" + StateThirdSubFlowInit flowpilot.StateName = "StateThirdSubFlowInit" + StateSecondSubFlowFinal flowpilot.StateName = "StateSecondSubFlowFinal" + StateFirstSubFlowInit flowpilot.StateName = "StateFirstSubFlowInit" + StateSignInOrSignUp flowpilot.StateName = "init" StateError flowpilot.StateName = "error" StateSuccess flowpilot.StateName = "success" @@ -20,6 +25,12 @@ const ( ) const ( + ActionEndSubFlow flowpilot.ActionName = "EndSubFlow" + ActionContinueToFinal flowpilot.ActionName = "ContinueToFinal" + ActionStartFirstSubFlow flowpilot.ActionName = "StartFirstSubFlow" + ActionStartSecondSubFlow flowpilot.ActionName = "StartSecondSubFlow" + ActionStartThirdSubFlow flowpilot.ActionName = "StartThirdSubFlow" + ActionSubmitEmail flowpilot.ActionName = "submit_email" ActionGetWAChallenge flowpilot.ActionName = "get_webauthn_challenge" ActionVerifyWAPublicKey flowpilot.ActionName = "verify_webauthn_public_key" diff --git a/backend/flowpilot/action_input.go b/backend/flowpilot/action_input.go new file mode 100644 index 000000000..88b3e80f6 --- /dev/null +++ b/backend/flowpilot/action_input.go @@ -0,0 +1,20 @@ +package flowpilot + +import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + +type ActionInput interface { + jsonmanager.JSONManager +} + +type ReadOnlyActionInput interface { + jsonmanager.ReadOnlyJSONManager +} + +// NewActionInput creates a new instance of ActionInput with empty JSON data. +func NewActionInput() ActionInput { + return jsonmanager.NewJSONManager() +} + +func NewActionInputFromString(data string) (ActionInput, error) { + return jsonmanager.NewJSONManagerFromString(data) +} diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index fb2eacfc1..af0411f36 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -139,10 +139,10 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { fb.addDefaultStates(fb.initialState, fb.errorState, fb.endState) if err := fb.scanStateActions(fb.flow, fb.subFlows); err != nil { - return nil, err + return nil, fmt.Errorf("failed to scan flow states: %w", err) } - return &defaultFlow{ + f := defaultFlow{ path: fb.path, flow: fb.flow, initialState: fb.initialState, @@ -152,7 +152,9 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { stateDetails: fb.stateDetails, ttl: fb.ttl, debug: fb.debug, - }, nil + } + + return &f, nil } // MustBuild constructs and returns the Flow object, panics on error. @@ -198,11 +200,13 @@ func (sfb *defaultSubFlowBuilder) FixedStates(initialState StateName) SubFlowBui func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { sfb.addDefaultStates(sfb.initialState) - return &defaultFlow{ + f := defaultFlow{ flow: sfb.flow, initialState: sfb.initialState, subFlows: sfb.subFlows, - }, nil + } + + return &f, nil } // MustBuild constructs and returns the SubFlow object, panics on error. diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 2989ad34d..d9c9661d9 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -16,9 +16,9 @@ type flowContext interface { // GetPath returns the current path within the flow. GetPath() string // Payload returns the JSONManager for accessing payload data. - Payload() utils.Payload + Payload() Payload // Stash returns the JSONManager for accessing stash data. - Stash() utils.Stash + Stash() Stash // GetInitialState returns the initial state of the flow. GetInitialState() StateName // GetCurrentState returns the current state of the flow. @@ -40,7 +40,7 @@ type actionInitializationContext interface { // AddInputs adds input parameters to the schema. AddInputs(inputs ...Input) // Stash returns the ReadOnlyJSONManager for accessing stash data. - Stash() utils.Stash + Stash() Stash // SuspendAction suspends the current action's execution. SuspendAction() } @@ -54,7 +54,7 @@ type actionExecutionContext interface { // TODO: FetchActionInput (for a action name) is maybe useless and can be removed or replaced. // FetchActionInput fetches input data for a specific action. - FetchActionInput(actionName ActionName) (utils.ReadOnlyActionInput, error) + FetchActionInput(actionName ActionName) (ReadOnlyActionInput, error) // ValidateInputData validates the input data against the schema. ValidateInputData() bool // CopyInputValuesToStash copies specified inputs to the stash. @@ -72,7 +72,8 @@ type actionExecutionContinuationContext interface { StartSubFlow(initState StateName, nextStates ...StateName) error // EndSubFlow ends the sub-flow and continues the flow execution to the previously specified next states. EndSubFlow() error - // TODO: Implement a function to step back to the previous state (while skipping self-transitions and recalling preserved data). + // ContinueToPreviousState rewinds the flow back to the previous state. + ContinueToPreviousState() error } // InitializationContext is a shorthand for actionInitializationContext within the flow initialization method. @@ -105,8 +106,8 @@ func createAndInitializeFlow(db FlowDB, flow defaultFlow) (FlowResult, error) { expiresAt := time.Now().Add(flow.ttl).UTC() // Initialize JSONManagers for stash and payload. - stash := utils.NewStash() - payload := utils.NewPayload() + stash := NewStash() + payload := NewPayload() // Create a new flow model with the provided parameters. flowCreation := flowCreationParam{currentState: flow.initialState, expiresAt: expiresAt} @@ -153,13 +154,13 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions } // Parse stash data from the flow model. - stash, err := utils.NewStashFromString(flowModel.StashData) + stash, err := NewStashFromString(flowModel.StashData) if err != nil { return nil, fmt.Errorf("failed to parse stash from flow: %w", err) } // Initialize JSONManagers for payload and flash data. - payload := utils.NewPayload() + payload := NewPayload() // Create a defaultFlowContext instance. fc := defaultFlowContext{ @@ -177,7 +178,7 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions // Parse raw input data into JSONManager. raw := options.inputData.getJSONStringOrDefault() - inputJSON, err := utils.NewActionInputFromString(raw) + inputJSON, err := NewActionInputFromString(raw) if err != nil { return nil, fmt.Errorf("failed to parse input data: %w", err) } diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 96a7f8042..bf0fb7a7f 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -3,7 +3,6 @@ package flowpilot import ( "errors" "fmt" - "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // defaultActionExecutionContext is the default implementation of the actionExecutionContext interface. @@ -59,22 +58,40 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio } // continueFlow continues the flow execution to the specified nextState with an optional error type. -func (aec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError, skipFlowValidation bool) error { - detail, err := aec.flow.getStateDetail(aec.flowModel.CurrentState) +func (aec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { + currentState := aec.flowModel.CurrentState + + detail, err := aec.flow.getStateDetail(currentState) if err != nil { - return err + return fmt.Errorf("invalid current state: %w", err) } - if !skipFlowValidation { - // Check if the specified nextState is valid. - if _, ok := detail.flow[nextState]; !(ok || - detail.subFlows.isEntryStateAllowed(nextState) || - nextState == aec.flow.endState || - nextState == aec.flow.errorState) { - return fmt.Errorf("progression to the specified state '%s' is not allowed", nextState) + stateExists := detail.flow.stateExists(nextState) + subFlowEntryStateAllowed := detail.subFlows.isEntryStateAllowed(nextState) + + // Check if the specified nextState is valid. + if !(stateExists || + subFlowEntryStateAllowed || + nextState == aec.flow.endState || + nextState == aec.flow.errorState) { + return fmt.Errorf("progression to the specified state '%s' is not allowed", nextState) + } + + if currentState != nextState { + err = aec.stash.addStateToHistory(currentState, nil, nil) + if err != nil { + return fmt.Errorf("failed to add the current state to the history: %w", err) } } + return aec.closeExecutionContext(nextState, flowError) +} + +func (aec *defaultActionExecutionContext) closeExecutionContext(nextState StateName, flowError FlowError) error { + if aec.executionResult != nil { + return errors.New("execution context is closed already") + } + // Prepare the result for continuing the flow. actionResult := actionExecutionResult{ actionName: aec.actionName, @@ -103,7 +120,7 @@ func (aec *defaultActionExecutionContext) Input() ExecutionSchema { } // Payload returns the JSONManager for accessing payload data. -func (aec *defaultActionExecutionContext) Payload() utils.Payload { +func (aec *defaultActionExecutionContext) Payload() Payload { return aec.payload } @@ -115,6 +132,7 @@ func (aec *defaultActionExecutionContext) CopyInputValuesToStash(inputNames ...s return err } } + return nil } @@ -125,43 +143,82 @@ func (aec *defaultActionExecutionContext) ValidateInputData() bool { // ContinueFlow continues the flow execution to the specified nextState. func (aec *defaultActionExecutionContext) ContinueFlow(nextState StateName) error { - return aec.continueFlow(nextState, nil, false) + return aec.continueFlow(nextState, nil) } // ContinueFlowWithError continues the flow execution to the specified nextState with an error type. func (aec *defaultActionExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { - return aec.continueFlow(nextState, flowErr, false) + return aec.continueFlow(nextState, flowErr) +} + +// ContinueToPreviousState continues the flow back to the previous state. +func (aec *defaultActionExecutionContext) ContinueToPreviousState() error { + nextState, unscheduledState, numOfScheduledStates, err := aec.stash.getLastStateFromHistory() + if err != nil { + return fmt.Errorf("failed get last state from history: %w", err) + } + + err = aec.stash.removeLastStateFromHistory() + if err != nil { + return fmt.Errorf("failed remove last state from history: %w", err) + } + + if nextState == nil { + nextState = &aec.flow.initialState + } + + if unscheduledState != nil { + err = aec.stash.addScheduledStates(*nextState) + if err != nil { + return fmt.Errorf("failed add scheduled states: %w", err) + } + } + + if numOfScheduledStates != nil { + for range make([]struct{}, *numOfScheduledStates) { + _, err = aec.stash.removeLastScheduledState() + if err != nil { + return fmt.Errorf("failed remove last scheduled state: %w", err) + } + } + } + + return aec.closeExecutionContext(*nextState, nil) } // StartSubFlow initiates the sub-flow associated with the specified StateName of the entry state (first parameter). // After a sub-flow action calls EndSubFlow(), the flow progresses to a state within the current flow or another // sub-flow's entry state, as specified in the list of nextStates (every StateName passed after the first parameter). func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nextStates ...StateName) error { - detail, err := aec.flow.getStateDetail(aec.flowModel.CurrentState) + currentState := aec.flowModel.CurrentState + + detail, err := aec.flow.getStateDetail(currentState) if err != nil { - return err + return fmt.Errorf("invalid current state: %w", err) } // the specified entry state must be an entry state to a sub-flow of the current flow if entryStateAllowed := detail.subFlows.isEntryStateAllowed(entryState); !entryStateAllowed { - return errors.New("the specified entry state is not associated with a sub-flow of the current flow") + return fmt.Errorf("the specified entry state '%s' is not associated with a sub-flow of the current flow", entryState) } var scheduledStates []StateName + // validate the specified nextStates and append valid state to the list of scheduledStates for index, nextState := range nextStates { + stateExists := detail.flow.stateExists(nextState) subFlowEntryStateAllowed := detail.subFlows.isEntryStateAllowed(nextState) // validate the current next state if index == len(nextStates)-1 { // the last state must be a member of the current flow or a sub-flow entry state - if _, ok := detail.flow[nextState]; !ok && !subFlowEntryStateAllowed { - return errors.New("the last next state is not a sub-flow entry state or a state associated with the current flow") + if !stateExists && !subFlowEntryStateAllowed { + return fmt.Errorf("the last next state '%s' specified is not a sub-flow entry state or another state associated with the current flow", nextState) } } else { // every other state must be a sub-flow entry state if !subFlowEntryStateAllowed { - return fmt.Errorf("next state with index %d is not a sub-flow entry state", index) + return fmt.Errorf("the specified next state '%s' is not a sub-flow entry state of the current flow", nextState) } } @@ -169,50 +226,38 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex scheduledStates = append(scheduledStates, nextState) } - // get the current sub-flow stack from the stash - stack := aec.stash.Get("_.scheduled_states").Array() - - newStack := make([]StateName, len(stack)) - - for index := range newStack { - newStack[index] = StateName(stack[index].String()) + err = aec.stash.addScheduledStates(scheduledStates...) + if err != nil { + return fmt.Errorf("failed to stash scheduled states: %w", err) } - // prepend the states to the list of previously defined scheduled states - newStack = append(scheduledStates, newStack...) + numOfScheduledStates := int64(len(scheduledStates)) - err = aec.stash.Set("_.scheduled_states", newStack) + err = aec.stash.addStateToHistory(currentState, nil, &numOfScheduledStates) if err != nil { - return fmt.Errorf("failed to stash scheduled states while staring a sub-flow: %w", err) + return fmt.Errorf("failed to add state to history: %w", err) } - return aec.continueFlow(entryState, nil, false) + return aec.closeExecutionContext(entryState, nil) } -// EndSubFlow ends the current sub-flow and progresses the flow to the previously defined nextStates (see StartSubFlow) +// EndSubFlow ends the current sub-flow and progresses the flow to the previously defined nextStates (see StartSubFlow()). func (aec *defaultActionExecutionContext) EndSubFlow() error { - // retrieve the previously scheduled states form the stash - stack := aec.stash.Get("_.scheduled_states").Array() + currentState := aec.flowModel.CurrentState - newStack := make([]StateName, len(stack)) - - for index := range newStack { - newStack[index] = StateName(stack[index].String()) - } - - // if there is no scheduled state left, continue to the end state - if len(newStack) == 0 { - newStack = append(newStack, aec.GetEndState()) + nextState, err := aec.stash.removeLastScheduledState() + if err != nil { + return fmt.Errorf("failed to end sub-flow: %w", err) } - // get and remove first stack item - nextState := newStack[0] - newStack = newStack[1:] - - // stash the updated list of scheduled states - if err := aec.stash.Set("_.scheduled_states", newStack); err != nil { - return fmt.Errorf("failed to stash scheduled states while ending the sub-flow: %w", err) + if nextState == nil { + nextState = &aec.flow.endState + } else { + err = aec.stash.addStateToHistory(currentState, nextState, nil) + if err != nil { + return fmt.Errorf("failed to add state to history: %w", err) + } } - return aec.continueFlow(nextState, nil, true) + return aec.closeExecutionContext(*nextState, nil) } diff --git a/backend/flowpilot/context_action_init.go b/backend/flowpilot/context_action_init.go index 4321c09fd..7ef9317f3 100644 --- a/backend/flowpilot/context_action_init.go +++ b/backend/flowpilot/context_action_init.go @@ -1,14 +1,10 @@ package flowpilot -import ( - "github.com/teamhanko/hanko/backend/flowpilot/utils" -) - // defaultActionInitializationContext is the default implementation of the actionInitializationContext interface. type defaultActionInitializationContext struct { schema InitializationSchema // InitializationSchema for action initialization. isSuspended bool // Flag indicating if the method is suspended. - stash utils.Stash // ReadOnlyJSONManager for accessing stash data. + stash Stash // ReadOnlyJSONManager for accessing stash data. } // AddInputs adds input data to the InitializationSchema. @@ -22,6 +18,6 @@ func (aic *defaultActionInitializationContext) SuspendAction() { } // Stash returns the ReadOnlyJSONManager for accessing stash data. -func (aic *defaultActionInitializationContext) Stash() utils.Stash { +func (aic *defaultActionInitializationContext) Stash() Stash { return aic.stash } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index a6ff516af..440632c9c 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -3,13 +3,12 @@ package flowpilot import ( "fmt" "github.com/gofrs/uuid" - "github.com/teamhanko/hanko/backend/flowpilot/utils" ) // defaultFlowContext is the default implementation of the flowContext interface. type defaultFlowContext struct { - payload utils.Payload // JSONManager for payload data. - stash utils.Stash // JSONManager for stash data. + payload Payload // JSONManager for payload data. + stash Stash // JSONManager for stash data. flow defaultFlow // The associated defaultFlow instance. dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. flowModel FlowModel // The current FlowModel. @@ -25,17 +24,17 @@ func (fc *defaultFlowContext) GetPath() string { return fc.flow.path } -// GetInitialState returns the initial addState of the defaultFlow. +// GetInitialState returns the initial state of the defaultFlow. func (fc *defaultFlowContext) GetInitialState() StateName { return fc.flow.initialState } -// GetCurrentState returns the current addState of the defaultFlow. +// GetCurrentState returns the current state of the defaultFlow. func (fc *defaultFlowContext) GetCurrentState() StateName { return fc.flowModel.CurrentState } -// CurrentStateEquals returns true, when one of the given stateNames matches the current addState name. +// CurrentStateEquals returns true, when one of the given stateNames matches the current state name. func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { for _, s := range stateNames { if s == fc.flowModel.CurrentState { @@ -46,34 +45,45 @@ func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { return false } -// GetPreviousState returns a pointer to the previous addState of the flow. +// GetPreviousState returns a pointer to the previous state of the flow. func (fc *defaultFlowContext) GetPreviousState() *StateName { - // TODO: A new state history logic needs to be implemented to reintroduce the functionality - return nil + state, _, _, _ := fc.stash.getLastStateFromHistory() + + if state == nil { + state = &fc.flow.initialState + } + + return state } -// GetErrorState returns the designated error addState of the flow. +// GetErrorState returns the designated error state of the flow. func (fc *defaultFlowContext) GetErrorState() StateName { return fc.flow.errorState } -// GetEndState returns the final addState of the flow. +// GetEndState returns the final state of the flow. func (fc *defaultFlowContext) GetEndState() StateName { return fc.flow.endState } // Stash returns the JSONManager for accessing stash data. -func (fc *defaultFlowContext) Stash() utils.Stash { +func (fc *defaultFlowContext) Stash() Stash { return fc.stash } -// StateExists checks if a given addState exists within the flow. +// StateExists checks if a given state exists within the current (sub-)flow. func (fc *defaultFlowContext) StateExists(stateName StateName) bool { - return fc.flow.stateExists(stateName) + detail, _ := fc.flow.getStateDetail(fc.flowModel.CurrentState) + + if detail != nil { + return detail.flow.stateExists(stateName) + } + + return false } // FetchActionInput fetches input data for a specific action. -func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (utils.ReadOnlyActionInput, error) { +func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (ReadOnlyActionInput, error) { // Find the last Transition with the specified method from the database wrapper. t, err := fc.dbw.FindLastTransitionWithAction(fc.flowModel.ID, methodName) if err != nil { @@ -82,11 +92,11 @@ func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (utils.Rea // If no Transition is found, return an empty JSONManager. if t == nil { - return utils.NewActionInput(), nil + return NewActionInput(), nil } // Parse input data from the Transition. - inputData, err := utils.NewActionInputFromString(t.InputData) + inputData, err := NewActionInputFromString(t.InputData) if err != nil { return nil, fmt.Errorf("failed to decode Transition data: %w", err) } diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index ff5c246e0..e5615ea64 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -46,8 +46,6 @@ type StateName string // ActionName represents the name of an action associated with a Transition. type ActionName string -// TODO: Should it be possible to partially implement the Action interface? E.g. when a action does not require initialization. - // Action defines the interface for flow actions. type Action interface { GetName() ActionName // Get the action name. @@ -56,6 +54,7 @@ type Action interface { Execute(ExecutionContext) error // Execute the action. } +// Actions represents a list of Action type Actions []Action // Transition holds an action associated with a state transition. @@ -63,6 +62,7 @@ type Transition struct { Action Action } +// getByName return the Action with the specified name. func (a Actions) getByName(name ActionName) (Action, error) { for _, action := range a { currentName := action.GetName() @@ -78,9 +78,9 @@ func (a Actions) getByName(name ActionName) (Action, error) { // Transitions is a collection of Transition instances. type Transitions []Transition -// getActions returns the Action associated with the specified name. -func (ts *Transitions) getActions() []Action { - var actions []Action +// getActions returns the Actions associated with the transition. +func (ts *Transitions) getActions() Actions { + var actions Actions for _, t := range *ts { actions = append(actions, t.Action) @@ -89,6 +89,7 @@ func (ts *Transitions) getActions() []Action { return actions } +// stateDetail represents details for a state, including the associated flow, available sub-flows and eligible actions. type stateDetail struct { flow StateTransitions subFlows SubFlows @@ -101,6 +102,12 @@ type stateDetails map[StateName]stateDetail // StateTransitions maps states to associated Transitions. type StateTransitions map[StateName]Transitions +// stateExists checks if a state exists in the flow. +func (st StateTransitions) stateExists(stateName StateName) bool { + _, ok := st[stateName] + return ok +} + // SubFlows maps a sub-flow init state to StateTransitions. type SubFlows map[StateName]SubFlow @@ -117,12 +124,12 @@ func (sfs SubFlows) isEntryStateAllowed(entryState StateName) bool { } type flow interface { - stateExists(stateName StateName) bool getStateDetail(stateName StateName) (*stateDetail, error) getSubFlows() SubFlows getFlow() StateTransitions } +// Flow represents a flow. type Flow interface { Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) ResultFromError(err error) FlowResult @@ -149,12 +156,6 @@ type defaultFlow struct { debug bool // Enables debug mode. } -// stateExists checks if a state exists in the defaultFlow. -func (f *defaultFlow) stateExists(stateName StateName) bool { - _, ok := f.flow[stateName] - return ok -} - // getActionsForState returns transitions for a specified state. func (f *defaultFlow) getStateDetail(stateName StateName) (*stateDetail, error) { if detail, ok := f.stateDetails[stateName]; ok { @@ -196,13 +197,13 @@ func (f *defaultFlow) validate() error { if len(f.endState) == 0 { return errors.New("fixed state 'endState' is not set") } - if !f.stateExists(f.initialState) { + if !f.flow.stateExists(f.initialState) { return errors.New("fixed state 'initialState' does not belong to the flow") } - if !f.stateExists(f.errorState) { + if !f.flow.stateExists(f.errorState) { return errors.New("fixed state 'errorState' does not belong to the flow") } - if !f.stateExists(f.endState) { + if !f.flow.stateExists(f.endState) { return errors.New("fixed state 'endState' does not belong to the flow") } if detail, _ := f.getStateDetail(f.endState); detail == nil || len(detail.actions) > 0 { diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go index a6bac44af..868cfcd71 100644 --- a/backend/flowpilot/input.go +++ b/backend/flowpilot/input.go @@ -1,7 +1,6 @@ package flowpilot import ( - "github.com/teamhanko/hanko/backend/flowpilot/utils" "regexp" ) @@ -34,7 +33,7 @@ type Input interface { shouldPersist() bool shouldPreserve() bool isIncludedOnState(stateName StateName) bool - validate(stateName StateName, inputData utils.ReadOnlyActionInput, stashData utils.Stash) bool + validate(stateName StateName, inputData ReadOnlyActionInput, stashData Stash) bool toPublicInput() *PublicInput } @@ -192,7 +191,7 @@ func (i *DefaultInput) shouldPreserve() bool { } // validate performs validation on the input field. -func (i *DefaultInput) validate(stateName StateName, inputData utils.ReadOnlyActionInput, stashData utils.Stash) bool { +func (i *DefaultInput) validate(stateName StateName, inputData ReadOnlyActionInput, stashData Stash) bool { // TODO: Replace with more structured validation logic. var inputValue *string diff --git a/backend/flowpilot/input_schema.go b/backend/flowpilot/input_schema.go index 43e0acd0f..97c18a5b4 100644 --- a/backend/flowpilot/input_schema.go +++ b/backend/flowpilot/input_schema.go @@ -1,7 +1,6 @@ package flowpilot import ( - "github.com/teamhanko/hanko/backend/flowpilot/utils" "github.com/tidwall/gjson" ) @@ -17,9 +16,9 @@ type ExecutionSchema interface { SetError(inputName string, inputError InputError) getInput(name string) Input - getOutputData() utils.ReadOnlyActionInput - getDataToPersist() utils.ReadOnlyActionInput - validateInputData(stateName StateName, stash utils.Stash) bool + getOutputData() ReadOnlyActionInput + getDataToPersist() ReadOnlyActionInput + validateInputData(stateName StateName, stash Stash) bool toInitializationSchema() InitializationSchema toPublicSchema(stateName StateName) PublicSchema } @@ -33,13 +32,13 @@ type PublicSchema []*PublicInput // defaultSchema implements the InitializationSchema interface and holds a collection of input fields. type defaultSchema struct { inputs - inputData utils.ReadOnlyActionInput - outputData utils.ActionInput + inputData ReadOnlyActionInput + outputData ActionInput } // newSchemaWithInputData creates a new ExecutionSchema with input data. -func newSchemaWithInputData(inputData utils.ActionInput) ExecutionSchema { - outputData := utils.NewActionInput() +func newSchemaWithInputData(inputData ActionInput) ExecutionSchema { + outputData := NewActionInput() return &defaultSchema{ inputData: inputData, @@ -48,8 +47,8 @@ func newSchemaWithInputData(inputData utils.ActionInput) ExecutionSchema { } // newSchemaWithInputData creates a new ExecutionSchema with input data. -func newSchemaWithOutputData(outputData utils.ReadOnlyActionInput) (ExecutionSchema, error) { - data, err := utils.NewActionInputFromString(outputData.String()) +func newSchemaWithOutputData(outputData ReadOnlyActionInput) (ExecutionSchema, error) { + data, err := NewActionInputFromString(outputData.String()) if err != nil { return nil, err } @@ -62,7 +61,7 @@ func newSchemaWithOutputData(outputData utils.ReadOnlyActionInput) (ExecutionSch // newSchema creates a new ExecutionSchema with no input data. func newSchema() ExecutionSchema { - inputData := utils.NewActionInput() + inputData := NewActionInput() return newSchemaWithInputData(inputData) } @@ -107,7 +106,7 @@ func (s *defaultSchema) SetError(inputName string, inputError InputError) { } // validateInputData validates the input data based on the input definitions in the schema. -func (s *defaultSchema) validateInputData(stateName StateName, stash utils.Stash) bool { +func (s *defaultSchema) validateInputData(stateName StateName, stash Stash) bool { valid := true for _, input := range s.inputs { @@ -120,8 +119,8 @@ func (s *defaultSchema) validateInputData(stateName StateName, stash utils.Stash } // getDataToPersist filters and returns data that should be persisted based on schema definitions. -func (s *defaultSchema) getDataToPersist() utils.ReadOnlyActionInput { - toPersist := utils.NewActionInput() +func (s *defaultSchema) getDataToPersist() ReadOnlyActionInput { + toPersist := NewActionInput() for _, input := range s.inputs { if v := s.inputData.Get(input.getName()); v.Exists() && input.shouldPersist() { @@ -133,7 +132,7 @@ func (s *defaultSchema) getDataToPersist() utils.ReadOnlyActionInput { } // getOutputData returns the output data from the schema. -func (s *defaultSchema) getOutputData() utils.ReadOnlyActionInput { +func (s *defaultSchema) getOutputData() ReadOnlyActionInput { return s.outputData } diff --git a/backend/flowpilot/payload.go b/backend/flowpilot/payload.go new file mode 100644 index 000000000..f6a7476ab --- /dev/null +++ b/backend/flowpilot/payload.go @@ -0,0 +1,17 @@ +package flowpilot + +import "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" + +type Payload interface { + jsonmanager.JSONManager +} + +// NewPayload creates a new instance of Payload with empty JSON data. +func NewPayload() Payload { + return jsonmanager.NewJSONManager() +} + +// NewPayloadFromString creates a new instance of Payload with the given JSON data. +func NewPayloadFromString(data string) (Payload, error) { + return jsonmanager.NewJSONManagerFromString(data) +} diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index 2faf51f63..0170e7bff 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -18,8 +18,8 @@ type PublicAction struct { type PublicActions []PublicAction // Add adds a link to the collection of PublicActions. -func (ls *PublicActions) Add(l PublicAction) { - *ls = append(*ls, l) +func (pa *PublicActions) Add(publicAction PublicAction) { + *pa = append(*pa, publicAction) } // PublicError represents an error for public exposure. diff --git a/backend/flowpilot/stash.go b/backend/flowpilot/stash.go new file mode 100644 index 000000000..37db0f0db --- /dev/null +++ b/backend/flowpilot/stash.go @@ -0,0 +1,182 @@ +package flowpilot + +import ( + "errors" + "fmt" + "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" +) + +// Stash defines the interface for managing JSON data. +type Stash interface { + jsonmanager.JSONManager + + getLastStateFromHistory() (state, unscheduledState *StateName, numOfScheduledStates *int64, err error) + addStateToHistory(state StateName, unscheduledState *StateName, numOfScheduledStates *int64) error + removeLastStateFromHistory() error + addScheduledStates(scheduledStates ...StateName) error + removeLastScheduledState() (*StateName, error) +} + +// defaultStash implements the Stash interface. +type defaultStash struct { + jsonmanager.JSONManager +} + +// NewStash creates a new instance of Stash with empty JSON data. +func NewStash() Stash { + return &defaultStash{JSONManager: jsonmanager.NewJSONManager()} +} + +// NewStashFromString creates a new instance of Stash with the given JSON data. +func NewStashFromString(data string) (Stash, error) { + jm, err := jsonmanager.NewJSONManagerFromString(data) + return &defaultStash{JSONManager: jm}, err +} + +// addStateToHistory adds a state to the history. Specify the values for unscheduledState and numOfScheduledStates to +// maintain the list of scheduled states if sub-flows are involved. +func (s *defaultStash) addStateToHistory(state StateName, unscheduledState *StateName, numOfScheduledStates *int64) error { + // Create a new JSONManager to manage the history item + historyItem := jsonmanager.NewJSONManager() + + // Get the last state from history + lastState, _, _, err := s.getLastStateFromHistory() + if err != nil { + return err + } + + // If the last state is the same as the new state, do not add it again + if lastState != nil && *lastState == state { + return nil + } + + // Set the state in the history item + if err = historyItem.Set("s", state); err != nil { + return fmt.Errorf("failed to set state: %w", err) + } + + // If numOfScheduledStates is provided and greater than 0, set it in the history item + if numOfScheduledStates != nil && *numOfScheduledStates > 0 { + if err = historyItem.Set("n", *numOfScheduledStates); err != nil { + return fmt.Errorf("failed to set num_of_scheduled_states: %w", err) + } + } + + // If unscheduledState is provided, set it in the history item + if unscheduledState != nil { + if err = historyItem.Set("u", *unscheduledState); err != nil { + return fmt.Errorf("failed to set unscheduled_state: %w", err) + } + } + + // Update the stashed history with the new history item + if err = s.Set("_.state_history.-1", historyItem.Unmarshal()); err != nil { + return fmt.Errorf("failed to update stashed history: %w", err) + } + + return nil +} + +// removeLastStateFromHistory removes the last state from history. +func (s *defaultStash) removeLastStateFromHistory() error { + if err := s.Delete("_.state_history.-1"); err != nil { + return err + } + + return nil +} + +// getLastStateFromHistory returns the last state, as well as the values for unscheduledState and numOfScheduledStates. +// These values indicate that further states have been added or removed from the list of scheduled states during the +// last state. +func (s *defaultStash) getLastStateFromHistory() (state, unscheduledState *StateName, numOfScheduledStates *int64, err error) { + // Get the index of the last history item + lastItemPosition := s.Get("_.state_history.#").Int() - 1 + + // Retrieve the last history item + lastHistoryItem := s.Get(fmt.Sprintf("_.state_history.%d", lastItemPosition)) + + // If the last history item doesn't exist, return nil values and no error + if !lastHistoryItem.Exists() { + return nil, nil, nil, nil + } + + // Check if the last history item is an object + if !lastHistoryItem.IsObject() { + return nil, nil, nil, errors.New("last history item is not an object") + } + + // Check if the 's' field exists in the last history item + if !lastHistoryItem.Get("s").Exists() { + return nil, nil, nil, errors.New("last history item is missing a value for 'state'") + } + + // Parse 's' field and assign it to the 'state' variable + sn := StateName(lastHistoryItem.Get("s").String()) + state = &sn + + // Check if 'u' field exists in the last history item + if lastHistoryItem.Get("u").Exists() { + // Parse 'u' field and assign it to the 'unscheduledState' variable + usn := StateName(lastHistoryItem.Get("u").String()) + unscheduledState = &usn + } + + // Check if 'n' field exists in the last history item + if lastHistoryItem.Get("n").Exists() { + // Parse 'n' field and assign it to the 'numOfScheduledStates' variable + n := lastHistoryItem.Get("n").Int() + numOfScheduledStates = &n + } + + // Return the parsed values + return state, unscheduledState, numOfScheduledStates, nil +} + +// addScheduledStates adds scheduled states. +func (s *defaultStash) addScheduledStates(scheduledStates ...StateName) error { + // get the current sub-flow stack from the stash + stack := s.Get("_.scheduled_states").Array() + + newStack := make([]StateName, len(stack)) + + for index := range newStack { + newStack[index] = StateName(stack[index].String()) + } + + // prepend the scheduledStates to the list of previously defined scheduled states + newStack = append(scheduledStates, newStack...) + + if err := s.Set("_.scheduled_states", newStack); err != nil { + return fmt.Errorf("failed to set scheduled_states: %w", err) + } + + return nil +} + +// removeLastScheduledState removes and returns the last scheduled state if present. +func (s *defaultStash) removeLastScheduledState() (*StateName, error) { + // retrieve the previously scheduled states form the stash + stack := s.Get("_.scheduled_states").Array() + + newStack := make([]StateName, len(stack)) + + for index := range newStack { + newStack[index] = StateName(stack[index].String()) + } + + if len(newStack) == 0 { + return nil, nil + } + + // get and remove first stack item + nextState := newStack[0] + newStack = newStack[1:] + + // stash the updated list of scheduled states + if err := s.Set("_.scheduled_states", newStack); err != nil { + return nil, fmt.Errorf("failed to stash scheduled states while ending the sub-flow: %w", err) + } + + return &nextState, nil +} diff --git a/backend/flowpilot/utils/storage.go b/backend/flowpilot/utils/storage.go deleted file mode 100644 index a16438366..000000000 --- a/backend/flowpilot/utils/storage.go +++ /dev/null @@ -1,50 +0,0 @@ -package utils - -import ( - "github.com/teamhanko/hanko/backend/flowpilot/jsonmanager" -) - -type Stash interface { - jsonmanager.JSONManager -} - -// NewStash creates a new instance of Stash with empty JSON data. -func NewStash() Stash { - return jsonmanager.NewJSONManager() -} - -// NewStashFromString creates a new instance of Stash with the given JSON data. -func NewStashFromString(data string) (Stash, error) { - return jsonmanager.NewJSONManagerFromString(data) -} - -type Payload interface { - jsonmanager.JSONManager -} - -// NewPayload creates a new instance of Payload with empty JSON data. -func NewPayload() Payload { - return jsonmanager.NewJSONManager() -} - -// NewPayloadFromString creates a new instance of Payload with the given JSON data. -func NewPayloadFromString(data string) (Payload, error) { - return jsonmanager.NewJSONManagerFromString(data) -} - -type ActionInput interface { - jsonmanager.JSONManager -} - -type ReadOnlyActionInput interface { - jsonmanager.ReadOnlyJSONManager -} - -// NewActionInput creates a new instance of ActionInput with empty JSON data. -func NewActionInput() ActionInput { - return jsonmanager.NewJSONManager() -} - -func NewActionInputFromString(data string) (ActionInput, error) { - return jsonmanager.NewJSONManagerFromString(data) -} diff --git a/backend/persistence/migrations/20230810173315_create_flows.up.fizz b/backend/persistence/migrations/20230810173315_create_flows.up.fizz index b3b9b9eff..f612b077d 100644 --- a/backend/persistence/migrations/20230810173315_create_flows.up.fizz +++ b/backend/persistence/migrations/20230810173315_create_flows.up.fizz @@ -1,7 +1,7 @@ create_table("flows") { t.Column("id", "uuid", {primary: true}) t.Column("current_state", "string") - t.Column("stash_data", "string") + t.Column("stash_data", "string", {"size": 4096}) t.Column("version", "int") t.Column("completed", "bool", {"default": false}) t.Column("expires_at", "timestamp") From 0e85033648ac664a0bca0dba2399fa9d74560d99 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Mon, 16 Oct 2023 16:44:14 +0200 Subject: [PATCH 012/278] chore: sub-flows don't have an initial state anymore, some fixes, refactoring --- backend/flow_api_test/flow.go | 5 --- backend/flowpilot/builder.go | 53 ++++++++++++++++------- backend/flowpilot/context.go | 2 +- backend/flowpilot/context_action_exec.go | 12 +++--- backend/flowpilot/context_flow.go | 11 ++--- backend/flowpilot/flow.go | 54 +++--------------------- 6 files changed, 53 insertions(+), 84 deletions(-) diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index b56c0243a..96077a80c 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -7,19 +7,16 @@ import ( var ThirdSubFlow = flowpilot.NewSubFlow(). State(StateThirdSubFlowInit, EndSubFlow{}, Back{}). - FixedStates(StateThirdSubFlowInit). MustBuild() var SecondSubFlow = flowpilot.NewSubFlow(). State(StateSecondSubFlowInit, ContinueToFinal{}, Back{}). State(StateSecondSubFlowFinal, EndSubFlow{}, Back{}). - FixedStates(StateSecondSubFlowInit). MustBuild() var FirstSubFlow = flowpilot.NewSubFlow(). State(StateFirstSubFlowInit, StartSecondSubFlow{}, Back{}). SubFlows(SecondSubFlow). - FixedStates(StateFirstSubFlowInit). MustBuild() var Flow = flowpilot.NewFlow("/flow_api_login"). @@ -35,8 +32,6 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StatePasswordCreation, SubmitNewPassword{}). State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipPasskeyCreation{}). State(StateCreatePasskey, VerifyWAAssertion{}). - State(StateError). - State(StateSuccess). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). SubFlows(FirstSubFlow, ThirdSubFlow). TTL(time.Minute * 10). diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index af0411f36..47713f588 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -1,6 +1,7 @@ package flowpilot import ( + "errors" "fmt" "time" ) @@ -18,7 +19,6 @@ type FlowBuilder interface { type SubFlowBuilder interface { State(state StateName, actions ...Action) SubFlowBuilder SubFlows(subFlows ...SubFlow) SubFlowBuilder - FixedStates(initialState StateName) SubFlowBuilder Build() (SubFlow, error) MustBuild() SubFlow } @@ -44,7 +44,7 @@ type defaultFlowBuilder struct { // newFlowBuilderBase creates a new defaultFlowBuilderBase instance. func newFlowBuilderBase() defaultFlowBuilderBase { - return defaultFlowBuilderBase{flow: make(StateTransitions), subFlows: make(SubFlows), stateDetails: make(stateDetails)} + return defaultFlowBuilderBase{flow: make(StateTransitions), subFlows: make(SubFlows, 0), stateDetails: make(stateDetails)} } // NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path. @@ -71,10 +71,7 @@ func (fb *defaultFlowBuilderBase) addState(state StateName, actions ...Action) { } func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...SubFlow) { - for _, subFlow := range subFlows { - initialState := subFlow.getInitialState() - fb.subFlows[initialState] = subFlow - } + fb.subFlows = append(fb.subFlows, subFlows...) } func (fb *defaultFlowBuilderBase) addDefaultStates(states ...StateName) { @@ -134,6 +131,34 @@ func (fb *defaultFlowBuilder) scanStateActions(flow StateTransitions, subFlows S 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.initialState) == 0 { + return errors.New("fixed state 'initialState' is not set") + } + if len(fb.errorState) == 0 { + return errors.New("fixed state 'errorState' is not set") + } + if len(fb.endState) == 0 { + return errors.New("fixed state 'endState' is not set") + } + if !fb.flow.stateExists(fb.initialState) { + return errors.New("fixed state 'initialState' does not belong to the flow") + } + if !fb.flow.stateExists(fb.errorState) { + return errors.New("fixed state 'errorState' does not belong to the flow") + } + if !fb.flow.stateExists(fb.endState) { + return errors.New("fixed state 'endState' does not belong to the flow") + } + if transitions, ok := fb.flow[fb.endState]; ok && len(transitions.getActions()) > 0 { + return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", fb.endState) + } + + return nil +} + // Build constructs and returns the Flow object. func (fb *defaultFlowBuilder) Build() (Flow, error) { fb.addDefaultStates(fb.initialState, fb.errorState, fb.endState) @@ -142,6 +167,10 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { return nil, fmt.Errorf("failed to scan flow states: %w", err) } + if err := fb.validate(); err != nil { + return nil, fmt.Errorf("flow validation failed: %w", err) + } + f := defaultFlow{ path: fb.path, flow: fb.flow, @@ -190,20 +219,12 @@ func (sfb *defaultSubFlowBuilder) State(state StateName, actions ...Action) SubF return sfb } -// FixedStates sets the initial of the sub-flow. -func (sfb *defaultSubFlowBuilder) FixedStates(initialState StateName) SubFlowBuilder { - sfb.initialState = initialState - return sfb -} - // Build constructs and returns the SubFlow object. func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { - sfb.addDefaultStates(sfb.initialState) f := defaultFlow{ - flow: sfb.flow, - initialState: sfb.initialState, - subFlows: sfb.subFlows, + flow: sfb.flow, + subFlows: sfb.subFlows, } return &f, nil diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index d9c9661d9..d6d7973c3 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -26,7 +26,7 @@ type flowContext interface { // CurrentStateEquals returns true, when one of the given states matches the current state. CurrentStateEquals(states ...StateName) bool // GetPreviousState returns the previous state of the flow. - GetPreviousState() *StateName + GetPreviousState() (*StateName, error) // GetErrorState returns the designated error state of the flow. GetErrorState() StateName // GetEndState returns the final state of the flow. diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index bf0fb7a7f..66db85834 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -67,7 +67,7 @@ func (aec *defaultActionExecutionContext) continueFlow(nextState StateName, flow } stateExists := detail.flow.stateExists(nextState) - subFlowEntryStateAllowed := detail.subFlows.isEntryStateAllowed(nextState) + subFlowEntryStateAllowed := detail.subFlows.stateExists(nextState) // Check if the specified nextState is valid. if !(stateExists || @@ -168,7 +168,7 @@ func (aec *defaultActionExecutionContext) ContinueToPreviousState() error { } if unscheduledState != nil { - err = aec.stash.addScheduledStates(*nextState) + err = aec.stash.addScheduledStates(*unscheduledState) if err != nil { return fmt.Errorf("failed add scheduled states: %w", err) } @@ -198,7 +198,7 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex } // the specified entry state must be an entry state to a sub-flow of the current flow - if entryStateAllowed := detail.subFlows.isEntryStateAllowed(entryState); !entryStateAllowed { + if entryStateAllowed := detail.subFlows.stateExists(entryState); !entryStateAllowed { return fmt.Errorf("the specified entry state '%s' is not associated with a sub-flow of the current flow", entryState) } @@ -207,18 +207,18 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex // validate the specified nextStates and append valid state to the list of scheduledStates for index, nextState := range nextStates { stateExists := detail.flow.stateExists(nextState) - subFlowEntryStateAllowed := detail.subFlows.isEntryStateAllowed(nextState) + subFlowEntryStateAllowed := detail.subFlows.stateExists(nextState) // validate the current next state if index == len(nextStates)-1 { // the last state must be a member of the current flow or a sub-flow entry state if !stateExists && !subFlowEntryStateAllowed { - return fmt.Errorf("the last next state '%s' specified is not a sub-flow entry state or another state associated with the current flow", nextState) + return fmt.Errorf("the last next state '%s' specified is not a sub-flow state or another state associated with the current flow", nextState) } } else { // every other state must be a sub-flow entry state if !subFlowEntryStateAllowed { - return fmt.Errorf("the specified next state '%s' is not a sub-flow entry state of the current flow", nextState) + return fmt.Errorf("the specified next state '%s' is not a sub-flow state of the current flow", nextState) } } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index 440632c9c..e11152611 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -46,14 +46,9 @@ func (fc *defaultFlowContext) CurrentStateEquals(stateNames ...StateName) bool { } // GetPreviousState returns a pointer to the previous state of the flow. -func (fc *defaultFlowContext) GetPreviousState() *StateName { - state, _, _, _ := fc.stash.getLastStateFromHistory() - - if state == nil { - state = &fc.flow.initialState - } - - return state +func (fc *defaultFlowContext) GetPreviousState() (*StateName, error) { + state, _, _, err := fc.stash.getLastStateFromHistory() + return state, err } // GetErrorState returns the designated error state of the flow. diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index e5615ea64..cc24e676a 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -1,7 +1,6 @@ package flowpilot import ( - "errors" "fmt" "time" ) @@ -109,13 +108,12 @@ func (st StateTransitions) stateExists(stateName StateName) bool { } // SubFlows maps a sub-flow init state to StateTransitions. -type SubFlows map[StateName]SubFlow +type SubFlows []SubFlow -func (sfs SubFlows) isEntryStateAllowed(entryState StateName) bool { +// stateExists checks if the given state exists in a sub-flow of the current flow. +func (sfs SubFlows) stateExists(state StateName) bool { for _, subFlow := range sfs { - subFlowInitState := subFlow.getInitialState() - - if subFlowInitState == entryState { + if subFlow.getFlow().stateExists(state) { return true } } @@ -134,20 +132,18 @@ type Flow interface { Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) ResultFromError(err error) FlowResult setDefaults() - validate() error flow } type SubFlow interface { - getInitialState() StateName flow } // defaultFlow defines a flow structure with states, transitions, and settings. type defaultFlow struct { flow StateTransitions // State transitions mapping. - subFlows SubFlows // TODO - stateDetails stateDetails // + subFlows SubFlows // The sub-flows of the current flow. + stateDetails stateDetails // Maps state names to flow details. path string // flow path or identifier. initialState StateName // Initial state of the flow. errorState StateName // State representing errors. @@ -165,10 +161,6 @@ func (f *defaultFlow) getStateDetail(stateName StateName) (*stateDetail, error) return nil, fmt.Errorf("unknown state: %s", stateName) } -func (f *defaultFlow) getInitialState() StateName { - return f.initialState -} - func (f *defaultFlow) getSubFlows() SubFlows { return f.subFlows } @@ -184,35 +176,6 @@ func (f *defaultFlow) setDefaults() { } } -// TODO: validate while building the flow -// validate performs validation checks on the defaultFlow configuration. -func (f *defaultFlow) validate() error { - // Validate fixed states and their presence in the flow. - if len(f.initialState) == 0 { - return errors.New("fixed state 'initialState' is not set") - } - if len(f.errorState) == 0 { - return errors.New("fixed state 'errorState' is not set") - } - if len(f.endState) == 0 { - return errors.New("fixed state 'endState' is not set") - } - if !f.flow.stateExists(f.initialState) { - return errors.New("fixed state 'initialState' does not belong to the flow") - } - if !f.flow.stateExists(f.errorState) { - return errors.New("fixed state 'errorState' does not belong to the flow") - } - if !f.flow.stateExists(f.endState) { - return errors.New("fixed state 'endState' does not belong to the flow") - } - if detail, _ := f.getStateDetail(f.endState); detail == nil || len(detail.actions) > 0 { - return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", f.endState) - } - - return nil -} - // Execute handles the execution of actions for a defaultFlow. func (f *defaultFlow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) { // Process execution options. @@ -225,11 +188,6 @@ func (f *defaultFlow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (F // Set default values for flow settings. f.setDefaults() - // Perform validation checks on the flow configuration. - if err := f.validate(); err != nil { - return nil, fmt.Errorf("invalid flow: %w", err) - } - if len(executionOptions.action) == 0 { // If the action is empty, create a new flow. return createAndInitializeFlow(db, *f) From 37dc224221b3ca6d82c01ea2732f6e692bfa9337 Mon Sep 17 00:00:00 2001 From: bjoern-m <56024829+bjoern-m@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:24:04 +0200 Subject: [PATCH 013/278] Update backend/flowpilot/context_action_exec.go Co-authored-by: Frederic Jahn --- backend/flowpilot/context_action_exec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 66db85834..24b40fe27 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -216,7 +216,7 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex return fmt.Errorf("the last next state '%s' specified is not a sub-flow state or another state associated with the current flow", nextState) } } else { - // every other state must be a sub-flow entry state + // every other state must be a sub-flow state if !subFlowEntryStateAllowed { return fmt.Errorf("the specified next state '%s' is not a sub-flow state of the current flow", nextState) } From 1fc78d242b6a1acf962a905d450f23fbdd32c9bc Mon Sep 17 00:00:00 2001 From: bjoern-m <56024829+bjoern-m@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:24:10 +0200 Subject: [PATCH 014/278] Update backend/flowpilot/context_action_exec.go Co-authored-by: Frederic Jahn --- backend/flowpilot/context_action_exec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 24b40fe27..6a10414b8 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -211,7 +211,7 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex // validate the current next state if index == len(nextStates)-1 { - // the last state must be a member of the current flow or a sub-flow entry state + // the last state must be a member of the current flow or a sub-flow if !stateExists && !subFlowEntryStateAllowed { return fmt.Errorf("the last next state '%s' specified is not a sub-flow state or another state associated with the current flow", nextState) } From 2081be8178b1d39ae9997940d54a83db8d20b8a2 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Wed, 18 Oct 2023 00:58:04 +0200 Subject: [PATCH 015/278] chore: refactoring --- backend/flowpilot/action_input.go | 1 + backend/flowpilot/builder.go | 113 +++++++++--------- backend/flowpilot/context.go | 22 ++-- backend/flowpilot/context_action_exec.go | 140 ++++++++++++----------- backend/flowpilot/context_flow.go | 20 ++-- backend/flowpilot/flow.go | 75 ++++++------ backend/flowpilot/input.go | 2 +- backend/flowpilot/payload.go | 5 - backend/flowpilot/response.go | 18 +-- backend/flowpilot/stash.go | 58 +++++----- 10 files changed, 239 insertions(+), 215 deletions(-) diff --git a/backend/flowpilot/action_input.go b/backend/flowpilot/action_input.go index 88b3e80f6..532123196 100644 --- a/backend/flowpilot/action_input.go +++ b/backend/flowpilot/action_input.go @@ -15,6 +15,7 @@ func NewActionInput() ActionInput { return jsonmanager.NewJSONManager() } +// NewActionInputFromString creates a new instance of ActionInput with the given JSON data. func NewActionInputFromString(data string) (ActionInput, error) { return jsonmanager.NewJSONManagerFromString(data) } diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 47713f588..8200e8036 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -8,8 +8,8 @@ import ( type FlowBuilder interface { TTL(ttl time.Duration) FlowBuilder - State(state StateName, actions ...Action) FlowBuilder - FixedStates(initialState, errorState, finalState StateName) FlowBuilder + State(stateName StateName, actions ...Action) FlowBuilder + FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder Debug(enabled bool) FlowBuilder SubFlows(subFlows ...SubFlow) FlowBuilder Build() (Flow, error) @@ -17,7 +17,7 @@ type FlowBuilder interface { } type SubFlowBuilder interface { - State(state StateName, actions ...Action) SubFlowBuilder + State(stateName StateName, actions ...Action) SubFlowBuilder SubFlows(subFlows ...SubFlow) SubFlowBuilder Build() (SubFlow, error) MustBuild() SubFlow @@ -25,19 +25,19 @@ type SubFlowBuilder interface { // defaultFlowBuilderBase is the base flow builder struct. type defaultFlowBuilderBase struct { - flow StateTransitions - subFlows SubFlows - initialState StateName - stateDetails stateDetails + flow StateTransitions + subFlows SubFlows + initialStateName StateName + stateDetails stateDetails } // defaultFlowBuilder is a builder struct for creating a new Flow. type defaultFlowBuilder struct { - path string - ttl time.Duration - errorState StateName - endState StateName - debug bool + path string + ttl time.Duration + errorStateName StateName + endStateName StateName + debug bool defaultFlowBuilderBase } @@ -60,39 +60,39 @@ func (fb *defaultFlowBuilder) TTL(ttl time.Duration) FlowBuilder { return fb } -func (fb *defaultFlowBuilderBase) addState(state StateName, actions ...Action) { +func (fb *defaultFlowBuilderBase) addState(stateName StateName, actions ...Action) { var transitions Transitions for _, action := range actions { transitions = append(transitions, Transition{Action: action}) } - fb.flow[state] = transitions + fb.flow[stateName] = transitions } func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...SubFlow) { fb.subFlows = append(fb.subFlows, subFlows...) } -func (fb *defaultFlowBuilderBase) addDefaultStates(states ...StateName) { - for _, state := range states { - if _, ok := fb.flow[state]; !ok { - fb.addState(state) +func (fb *defaultFlowBuilderBase) addDefaultStates(stateNames ...StateName) { + for _, stateName := range stateNames { + if _, exists := fb.flow[stateName]; !exists { + fb.addState(stateName) } } } // State adds a new transition to the flow. -func (fb *defaultFlowBuilder) State(state StateName, actions ...Action) FlowBuilder { - fb.addState(state, actions...) +func (fb *defaultFlowBuilder) State(stateName StateName, actions ...Action) FlowBuilder { + fb.addState(stateName, actions...) return fb } // FixedStates sets the initial and final states of the flow. -func (fb *defaultFlowBuilder) FixedStates(initialState, errorState, finalState StateName) FlowBuilder { - fb.initialState = initialState - fb.errorState = errorState - fb.endState = finalState +func (fb *defaultFlowBuilder) FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder { + fb.initialStateName = initialStateName + fb.errorStateName = errorStateName + fb.endStateName = finalStateName return fb } @@ -107,23 +107,34 @@ func (fb *defaultFlowBuilder) Debug(enabled bool) FlowBuilder { return fb } -func (fb *defaultFlowBuilder) scanStateActions(flow StateTransitions, subFlows SubFlows) error { - for state, transitions := range flow { - if _, ok := fb.stateDetails[state]; ok { - return fmt.Errorf("flow state '%s' is not unique", state) +// 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 StateTransitions, subFlows SubFlows) error { + // Iterate through states in the flow. + for stateName, transitions := range flow { + // Check if state name is already in use. + if _, ok := fb.stateDetails[stateName]; ok { + return fmt.Errorf("non-unique flow state '%s'", stateName) } + // Retrieve associated actions for the state. actions := transitions.getActions() - fb.stateDetails[state] = stateDetail{ + // Create state details. + state := stateDetail{ + name: stateName, flow: flow, subFlows: subFlows, actions: actions, } + + // Store state details. + fb.stateDetails[stateName] = &state } + // Recursively scan sub-flows. for _, sf := range subFlows { - if err := fb.scanStateActions(sf.getFlow(), sf.getSubFlows()); err != nil { + if err := fb.scanFlowStates(sf.getFlow(), sf.getSubFlows()); err != nil { return err } } @@ -134,26 +145,26 @@ func (fb *defaultFlowBuilder) scanStateActions(flow StateTransitions, subFlows S // 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.initialState) == 0 { + if len(fb.initialStateName) == 0 { return errors.New("fixed state 'initialState' is not set") } - if len(fb.errorState) == 0 { + if len(fb.errorStateName) == 0 { return errors.New("fixed state 'errorState' is not set") } - if len(fb.endState) == 0 { + if len(fb.endStateName) == 0 { return errors.New("fixed state 'endState' is not set") } - if !fb.flow.stateExists(fb.initialState) { + if !fb.flow.stateExists(fb.initialStateName) { return errors.New("fixed state 'initialState' does not belong to the flow") } - if !fb.flow.stateExists(fb.errorState) { + if !fb.flow.stateExists(fb.errorStateName) { return errors.New("fixed state 'errorState' does not belong to the flow") } - if !fb.flow.stateExists(fb.endState) { + if !fb.flow.stateExists(fb.endStateName) { return errors.New("fixed state 'endState' does not belong to the flow") } - if transitions, ok := fb.flow[fb.endState]; ok && len(transitions.getActions()) > 0 { - return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", fb.endState) + if transitions, ok := fb.flow[fb.endStateName]; ok && len(transitions.getActions()) > 0 { + return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", fb.endStateName) } return nil @@ -161,9 +172,9 @@ func (fb *defaultFlowBuilder) validate() error { // Build constructs and returns the Flow object. func (fb *defaultFlowBuilder) Build() (Flow, error) { - fb.addDefaultStates(fb.initialState, fb.errorState, fb.endState) + fb.addDefaultStates(fb.initialStateName, fb.errorStateName, fb.endStateName) - if err := fb.scanStateActions(fb.flow, fb.subFlows); err != nil { + if err := fb.scanFlowStates(fb.flow, fb.subFlows); err != nil { return nil, fmt.Errorf("failed to scan flow states: %w", err) } @@ -172,15 +183,15 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { } f := defaultFlow{ - path: fb.path, - flow: fb.flow, - initialState: fb.initialState, - errorState: fb.errorState, - endState: fb.endState, - subFlows: fb.subFlows, - stateDetails: fb.stateDetails, - ttl: fb.ttl, - debug: fb.debug, + path: fb.path, + flow: fb.flow, + initialStateName: fb.initialStateName, + errorStateName: fb.errorStateName, + endStateName: fb.endStateName, + subFlows: fb.subFlows, + stateDetails: fb.stateDetails, + ttl: fb.ttl, + debug: fb.debug, } return &f, nil @@ -214,8 +225,8 @@ func (sfb *defaultSubFlowBuilder) SubFlows(subFlows ...SubFlow) SubFlowBuilder { } // State adds a new transition to the flow. -func (sfb *defaultSubFlowBuilder) State(state StateName, actions ...Action) SubFlowBuilder { - sfb.addState(state, actions...) +func (sfb *defaultSubFlowBuilder) State(stateName StateName, actions ...Action) SubFlowBuilder { + sfb.addState(stateName, actions...) return sfb } diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index d6d7973c3..a15db8f32 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -67,9 +67,9 @@ type actionExecutionContinuationContext interface { // ContinueFlow continues the flow execution to the specified next state. ContinueFlow(nextState StateName) error // ContinueFlowWithError continues the flow execution to the specified next state with an error. - ContinueFlowWithError(nextState StateName, flowErr FlowError) error + ContinueFlowWithError(nextStateName StateName, flowErr FlowError) error // StartSubFlow starts a sub-flow and continues the flow execution to the specified next states after the sub-flow has been ended. - StartSubFlow(initState StateName, nextStates ...StateName) error + StartSubFlow(initStateName StateName, nextStateNames ...StateName) error // EndSubFlow ends the sub-flow and continues the flow execution to the previously specified next states. EndSubFlow() error // ContinueToPreviousState rewinds the flow back to the previous state. @@ -110,7 +110,7 @@ func createAndInitializeFlow(db FlowDB, flow defaultFlow) (FlowResult, error) { payload := NewPayload() // Create a new flow model with the provided parameters. - flowCreation := flowCreationParam{currentState: flow.initialState, expiresAt: expiresAt} + flowCreation := flowCreationParam{currentState: flow.initialStateName, expiresAt: expiresAt} flowModel, err := dbw.CreateFlowWithParam(flowCreation) if err != nil { return nil, fmt.Errorf("failed to create flow: %w", err) @@ -126,7 +126,7 @@ func createAndInitializeFlow(db FlowDB, flow defaultFlow) (FlowResult, error) { } // Generate a response based on the execution result. - er := executionResult{nextState: flowModel.CurrentState} + er := executionResult{nextStateName: flowModel.CurrentState} return er.generateResponse(fc, flow.debug), nil } @@ -136,21 +136,21 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions // Parse the actionParam parameter to get the actionParam name and flow ID. actionParam, err := utils.ParseActionParam(options.action) if err != nil { - return newFlowResultFromError(flow.errorState, ErrorActionParamInvalid.Wrap(err), flow.debug), nil + return newFlowResultFromError(flow.errorStateName, ErrorActionParamInvalid.Wrap(err), flow.debug), nil } // Retrieve the flow model from the database using the flow ID. flowModel, err := db.GetFlow(actionParam.FlowID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil + return newFlowResultFromError(flow.errorStateName, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil } return nil, fmt.Errorf("failed to get flow: %w", err) } // Check if the flow has expired. if time.Now().After(flowModel.ExpiresAt) { - return newFlowResultFromError(flow.errorState, ErrorFlowExpired, flow.debug), nil + return newFlowResultFromError(flow.errorStateName, ErrorFlowExpired, flow.debug), nil } // Parse stash data from the flow model. @@ -171,7 +171,7 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions payload: payload, } - detail, err := flow.getStateDetail(flowModel.CurrentState) + state, err := flow.getState(flowModel.CurrentState) if err != nil { return nil, err } @@ -187,9 +187,9 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions actionName := ActionName(actionParam.ActionName) // Get the action associated with the actionParam name. - action, err := detail.actions.getByName(actionName) + action, err := state.getAction(actionName) if err != nil { - return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil + return newFlowResultFromError(flow.errorStateName, ErrorOperationNotPermitted.Wrap(err), flow.debug), nil } // Initialize the schema and action context for action execution. @@ -199,7 +199,7 @@ func executeFlowAction(db FlowDB, flow defaultFlow, options flowExecutionOptions // Check if the action is suspended. if aic.isSuspended { - return newFlowResultFromError(flow.errorState, ErrorOperationNotPermitted, flow.debug), nil + return newFlowResultFromError(flow.errorStateName, ErrorOperationNotPermitted, flow.debug), nil } // Create a actionExecutionContext instance for action execution. diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 6a10414b8..7872c1017 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -16,14 +16,14 @@ type defaultActionExecutionContext struct { // saveNextState updates the flow's state and saves Transition data after action execution. func (aec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { - completed := executionResult.nextState == aec.flow.endState + completed := executionResult.nextStateName == aec.flow.endStateName newVersion := aec.flowModel.Version + 1 stashData := aec.stash.String() // Prepare parameters for updating the flow in the database. flowUpdate := flowUpdateParam{ flowID: aec.flowModel.ID, - nextState: executionResult.nextState, + nextState: executionResult.nextStateName, stashData: stashData, version: newVersion, completed: completed, @@ -44,7 +44,7 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio flowID: aec.flowModel.ID, actionName: aec.actionName, fromState: aec.flowModel.CurrentState, - toState: executionResult.nextState, + toState: executionResult.nextStateName, inputData: inputDataToPersist, flowError: executionResult.flowError, } @@ -57,37 +57,36 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio return nil } -// continueFlow continues the flow execution to the specified nextState with an optional error type. -func (aec *defaultActionExecutionContext) continueFlow(nextState StateName, flowError FlowError) error { - currentState := aec.flowModel.CurrentState - - detail, err := aec.flow.getStateDetail(currentState) +// continueFlow continues the flow execution to the specified nextStateName with an optional error type. +func (aec *defaultActionExecutionContext) continueFlow(nextStateName StateName, flowError FlowError) error { + // Retrieve the current state from the flow. + currentState, err := aec.flow.getState(aec.flowModel.CurrentState) if err != nil { return fmt.Errorf("invalid current state: %w", err) } - stateExists := detail.flow.stateExists(nextState) - subFlowEntryStateAllowed := detail.subFlows.stateExists(nextState) + nextStateAllowed := currentState.flow.stateExists(nextStateName) - // Check if the specified nextState is valid. - if !(stateExists || - subFlowEntryStateAllowed || - nextState == aec.flow.endState || - nextState == aec.flow.errorState) { - return fmt.Errorf("progression to the specified state '%s' is not allowed", nextState) + // Check if the specified nextStateName is valid. + if !(nextStateAllowed || + nextStateName == aec.flow.endStateName || + nextStateName == aec.flow.errorStateName) { + return fmt.Errorf("progression to the specified state '%s' is not allowed", nextStateName) } - if currentState != nextState { - err = aec.stash.addStateToHistory(currentState, nil, nil) + // Add the current state to the execution history. + if currentState.name != nextStateName { + err = aec.stash.addStateToHistory(currentState.name, nil, nil) if err != nil { return fmt.Errorf("failed to add the current state to the history: %w", err) } } - return aec.closeExecutionContext(nextState, flowError) + // Close the execution context with the given next state. + return aec.closeExecutionContext(nextStateName, flowError) } -func (aec *defaultActionExecutionContext) closeExecutionContext(nextState StateName, flowError FlowError) error { +func (aec *defaultActionExecutionContext) closeExecutionContext(nextStateName StateName, flowError FlowError) error { if aec.executionResult != nil { return errors.New("execution context is closed already") } @@ -99,7 +98,7 @@ func (aec *defaultActionExecutionContext) closeExecutionContext(nextState StateN } result := executionResult{ - nextState: nextState, + nextStateName: nextStateName, flowError: flowError, actionExecutionResult: &actionResult, } @@ -141,32 +140,36 @@ func (aec *defaultActionExecutionContext) ValidateInputData() bool { return aec.input.validateInputData(aec.flowModel.CurrentState, aec.stash) } -// ContinueFlow continues the flow execution to the specified nextState. -func (aec *defaultActionExecutionContext) ContinueFlow(nextState StateName) error { - return aec.continueFlow(nextState, nil) +// ContinueFlow continues the flow execution to the specified nextStateName. +func (aec *defaultActionExecutionContext) ContinueFlow(nextStateName StateName) error { + return aec.continueFlow(nextStateName, nil) } -// ContinueFlowWithError continues the flow execution to the specified nextState with an error type. -func (aec *defaultActionExecutionContext) ContinueFlowWithError(nextState StateName, flowErr FlowError) error { - return aec.continueFlow(nextState, flowErr) +// ContinueFlowWithError continues the flow execution to the specified nextStateName with an error type. +func (aec *defaultActionExecutionContext) ContinueFlowWithError(nextStateName StateName, flowErr FlowError) error { + return aec.continueFlow(nextStateName, flowErr) } // ContinueToPreviousState continues the flow back to the previous state. func (aec *defaultActionExecutionContext) ContinueToPreviousState() error { - nextState, unscheduledState, numOfScheduledStates, err := aec.stash.getLastStateFromHistory() + // Get the last state, the unscheduled state, and the number of scheduled states from history. + lastStateName, unscheduledState, numOfScheduledStates, err := aec.stash.getLastStateFromHistory() if err != nil { return fmt.Errorf("failed get last state from history: %w", err) } + // Remove the last state from history. err = aec.stash.removeLastStateFromHistory() if err != nil { return fmt.Errorf("failed remove last state from history: %w", err) } - if nextState == nil { - nextState = &aec.flow.initialState + // If there was no last state, set it to the initial state. + if lastStateName == nil { + lastStateName = &aec.flow.initialStateName } + // Add the unscheduled state back to the scheduled states if available. if unscheduledState != nil { err = aec.stash.addScheduledStates(*unscheduledState) if err != nil { @@ -174,6 +177,7 @@ func (aec *defaultActionExecutionContext) ContinueToPreviousState() error { } } + // Remove any previously scheduled states from the schedule. if numOfScheduledStates != nil { for range make([]struct{}, *numOfScheduledStates) { _, err = aec.stash.removeLastScheduledState() @@ -183,49 +187,50 @@ func (aec *defaultActionExecutionContext) ContinueToPreviousState() error { } } - return aec.closeExecutionContext(*nextState, nil) + // Close the execution context with the last state. + return aec.closeExecutionContext(*lastStateName, nil) } -// StartSubFlow initiates the sub-flow associated with the specified StateName of the entry state (first parameter). -// After a sub-flow action calls EndSubFlow(), the flow progresses to a state within the current flow or another -// sub-flow's entry state, as specified in the list of nextStates (every StateName passed after the first parameter). -func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nextStates ...StateName) error { - currentState := aec.flowModel.CurrentState - - detail, err := aec.flow.getStateDetail(currentState) +// StartSubFlow initiates a sub-flow associated with the specified entryStateName (first parameter). When a sub-flow +// action calls EndSubFlow(), the flow progresses to a state within the current flow or another sub-flow's entry state, +// as specified in the list of nextStates (every StateName passed after the first parameter). +func (aec *defaultActionExecutionContext) StartSubFlow(entryStateName StateName, nextStateNames ...StateName) error { + // Retrieve the current state from the flow. + currentState, err := aec.flow.getState(aec.flowModel.CurrentState) if err != nil { return fmt.Errorf("invalid current state: %w", err) } - // the specified entry state must be an entry state to a sub-flow of the current flow - if entryStateAllowed := detail.subFlows.stateExists(entryState); !entryStateAllowed { - return fmt.Errorf("the specified entry state '%s' is not associated with a sub-flow of the current flow", entryState) + // Ensure the specified entry state is associated with a sub-flow of the current flow. + if !currentState.subFlows.stateExists(entryStateName) { + return fmt.Errorf("the specified entry state '%s' is not associated with a sub-flow of the current flow", entryStateName) } var scheduledStates []StateName - // validate the specified nextStates and append valid state to the list of scheduledStates - for index, nextState := range nextStates { - stateExists := detail.flow.stateExists(nextState) - subFlowEntryStateAllowed := detail.subFlows.stateExists(nextState) + // Validate the specified nextStates and append valid states to the list of scheduledStates. + for index, nextStateName := range nextStateNames { + stateExists := currentState.flow.stateExists(nextStateName) + subFlowStateExists := currentState.subFlows.stateExists(nextStateName) - // validate the current next state - if index == len(nextStates)-1 { - // the last state must be a member of the current flow or a sub-flow - if !stateExists && !subFlowEntryStateAllowed { - return fmt.Errorf("the last next state '%s' specified is not a sub-flow state or another state associated with the current flow", nextState) + // Validate the current next state. + if index == len(nextStateNames)-1 { + // The last state must be a member of the current flow or a sub-flow. + if !stateExists && !subFlowStateExists { + return fmt.Errorf("the last next state '%s' specified is not a sub-flow state or another state associated with the current flow", nextStateName) } } else { - // every other state must be a sub-flow state - if !subFlowEntryStateAllowed { - return fmt.Errorf("the specified next state '%s' is not a sub-flow state of the current flow", nextState) + // Every other state must be a sub-flow state. + if !subFlowStateExists { + return fmt.Errorf("the specified next state '%s' is not a sub-flow state of the current flow", nextStateName) } } - // append the current nextState to the list of scheduled states - scheduledStates = append(scheduledStates, nextState) + // Append the current next state to the list of scheduled states. + scheduledStates = append(scheduledStates, nextStateName) } + // Add the scheduled states to the stash. err = aec.stash.addScheduledStates(scheduledStates...) if err != nil { return fmt.Errorf("failed to stash scheduled states: %w", err) @@ -233,31 +238,38 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryState StateName, nex numOfScheduledStates := int64(len(scheduledStates)) - err = aec.stash.addStateToHistory(currentState, nil, &numOfScheduledStates) + // Add the current state to the execution history. + err = aec.stash.addStateToHistory(currentState.name, nil, &numOfScheduledStates) if err != nil { return fmt.Errorf("failed to add state to history: %w", err) } - return aec.closeExecutionContext(entryState, nil) + // Close the execution context with the entry state of the sub-flow. + return aec.closeExecutionContext(entryStateName, nil) } -// EndSubFlow ends the current sub-flow and progresses the flow to the previously defined nextStates (see StartSubFlow()). +// EndSubFlow ends the current sub-flow and progresses the flow to the previously defined next states. func (aec *defaultActionExecutionContext) EndSubFlow() error { - currentState := aec.flowModel.CurrentState + // Retrieve the name of the current state. + currentStateName := aec.flowModel.CurrentState - nextState, err := aec.stash.removeLastScheduledState() + // Attempt to remove the last scheduled state from the stash. + scheduledStateName, err := aec.stash.removeLastScheduledState() if err != nil { return fmt.Errorf("failed to end sub-flow: %w", err) } - if nextState == nil { - nextState = &aec.flow.endState + // If no scheduled state is available, set it to the end state. + if scheduledStateName == nil { + scheduledStateName = &aec.flow.endStateName } else { - err = aec.stash.addStateToHistory(currentState, nextState, nil) + // Add the current state to the execution history. + err = aec.stash.addStateToHistory(currentStateName, scheduledStateName, nil) if err != nil { return fmt.Errorf("failed to add state to history: %w", err) } } - return aec.closeExecutionContext(*nextState, nil) + // Close the execution context with the scheduled state. + return aec.closeExecutionContext(*scheduledStateName, nil) } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index e11152611..1f3a9b6f9 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -14,22 +14,22 @@ type defaultFlowContext struct { flowModel FlowModel // The current FlowModel. } -// GetFlowID returns the unique ID of the current defaultFlow. +// GetFlowID returns the unique ID of the current flow. func (fc *defaultFlowContext) GetFlowID() uuid.UUID { return fc.flowModel.ID } -// GetPath returns the current path within the defaultFlow. +// GetPath returns the current path within the flow. func (fc *defaultFlowContext) GetPath() string { return fc.flow.path } -// GetInitialState returns the initial state of the defaultFlow. +// GetInitialState returns the initial state of the flow. func (fc *defaultFlowContext) GetInitialState() StateName { - return fc.flow.initialState + return fc.flow.initialStateName } -// GetCurrentState returns the current state of the defaultFlow. +// GetCurrentState returns the current state of the flow. func (fc *defaultFlowContext) GetCurrentState() StateName { return fc.flowModel.CurrentState } @@ -53,12 +53,12 @@ func (fc *defaultFlowContext) GetPreviousState() (*StateName, error) { // GetErrorState returns the designated error state of the flow. func (fc *defaultFlowContext) GetErrorState() StateName { - return fc.flow.errorState + return fc.flow.errorStateName } // GetEndState returns the final state of the flow. func (fc *defaultFlowContext) GetEndState() StateName { - return fc.flow.endState + return fc.flow.endStateName } // Stash returns the JSONManager for accessing stash data. @@ -68,10 +68,10 @@ func (fc *defaultFlowContext) Stash() Stash { // StateExists checks if a given state exists within the current (sub-)flow. func (fc *defaultFlowContext) StateExists(stateName StateName) bool { - detail, _ := fc.flow.getStateDetail(fc.flowModel.CurrentState) + state, _ := fc.flow.getState(fc.flowModel.CurrentState) - if detail != nil { - return detail.flow.stateExists(stateName) + if state != nil { + return state.flow.stateExists(stateName) } return false diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index cc24e676a..f6f665066 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -19,7 +19,7 @@ func (i InputData) getJSONStringOrDefault() string { return i.JSONString } -// flowExecutionOptions represents options for executing a defaultFlow. +// flowExecutionOptions represents options for executing a flow. type flowExecutionOptions struct { action string inputData InputData @@ -39,7 +39,7 @@ func WithInputData(inputData InputData) func(*flowExecutionOptions) { } } -// StateName represents the name of a state in a defaultFlow. +// StateName represents the name of a state in a flow. type StateName string // ActionName represents the name of an action associated with a Transition. @@ -61,19 +61,6 @@ type Transition struct { Action Action } -// getByName return the Action with the specified name. -func (a Actions) getByName(name ActionName) (Action, error) { - for _, action := range a { - currentName := action.GetName() - - if currentName == name { - return action, nil - } - } - - return nil, fmt.Errorf("action '%s' not found", name) -} - // Transitions is a collection of Transition instances. type Transitions []Transition @@ -88,15 +75,29 @@ func (ts *Transitions) getActions() Actions { return actions } -// stateDetail represents details for a state, including the associated flow, available sub-flows and eligible actions. +// state represents details for a state, including the associated flow, available sub-flows and eligible actions. type stateDetail struct { + name StateName flow StateTransitions subFlows SubFlows actions Actions } +// getAction returns the Action with the specified name. +func (sd *stateDetail) getAction(actionName ActionName) (Action, error) { + for _, action := range sd.actions { + currentActionName := action.GetName() + + if currentActionName == actionName { + return action, nil + } + } + + return nil, fmt.Errorf("action '%s' not found", actionName) +} + // stateDetails maps states to associated Actions, flows and sub-flows. -type stateDetails map[StateName]stateDetail +type stateDetails map[StateName]*stateDetail // StateTransitions maps states to associated Transitions. type StateTransitions map[StateName]Transitions @@ -121,8 +122,9 @@ func (sfs SubFlows) stateExists(state StateName) bool { return false } -type flow interface { - getStateDetail(stateName StateName) (*stateDetail, error) +// flowBase represents the base of the flow interfaces. +type flowBase interface { + getState(stateName StateName) (*stateDetail, error) getSubFlows() SubFlows getFlow() StateTransitions } @@ -132,39 +134,42 @@ type Flow interface { Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) ResultFromError(err error) FlowResult setDefaults() - flow + flowBase } +// SubFlow represents a sub-flow. type SubFlow interface { - flow + flowBase } // defaultFlow defines a flow structure with states, transitions, and settings. type defaultFlow struct { - flow StateTransitions // State transitions mapping. - subFlows SubFlows // The sub-flows of the current flow. - stateDetails stateDetails // Maps state names to flow details. - path string // flow path or identifier. - initialState StateName // Initial state of the flow. - errorState StateName // State representing errors. - endState StateName // Final state of the flow. - ttl time.Duration // Time-to-live for the flow. - debug bool // Enables debug mode. + flow StateTransitions // StateName transitions mapping. + subFlows SubFlows // The sub-flows of the current flow. + stateDetails stateDetails // Maps state names to flow details. + path string // flow path or identifier. + initialStateName StateName // Initial state of the flow. + errorStateName StateName // State representing errors. + endStateName StateName // Final state of the flow. + ttl time.Duration // Time-to-live for the flow. + debug bool // Enables debug mode. } // getActionsForState returns transitions for a specified state. -func (f *defaultFlow) getStateDetail(stateName StateName) (*stateDetail, error) { - if detail, ok := f.stateDetails[stateName]; ok { - return &detail, nil +func (f *defaultFlow) getState(stateName StateName) (*stateDetail, error) { + if state, ok := f.stateDetails[stateName]; ok { + return state, nil } return nil, fmt.Errorf("unknown state: %s", stateName) } +// getSubFlows returns the sub-flows of the current flow. func (f *defaultFlow) getSubFlows() SubFlows { return f.subFlows } +// getFlow returns the state to action mapping of the current flow. func (f *defaultFlow) getFlow() StateTransitions { return f.flow } @@ -198,7 +203,7 @@ func (f *defaultFlow) Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (F } // ResultFromError returns an error response for the defaultFlow. -func (f *defaultFlow) ResultFromError(err error) (result FlowResult) { +func (f *defaultFlow) ResultFromError(err error) FlowResult { flowError := ErrorTechnical if e, ok := err.(FlowError); ok { @@ -207,5 +212,5 @@ func (f *defaultFlow) ResultFromError(err error) (result FlowResult) { flowError = flowError.Wrap(err) } - return newFlowResultFromError(f.errorState, flowError, f.debug) + return newFlowResultFromError(f.errorStateName, flowError, f.debug) } diff --git a/backend/flowpilot/input.go b/backend/flowpilot/input.go index 868cfcd71..a63a59195 100644 --- a/backend/flowpilot/input.go +++ b/backend/flowpilot/input.go @@ -24,7 +24,7 @@ type Input interface { Hidden(b bool) Input Preserve(b bool) Input Persist(b bool) Input - ConditionalIncludeOnState(states ...StateName) Input + ConditionalIncludeOnState(stateNames ...StateName) Input CompareWithStash(b bool) Input setValue(value interface{}) Input diff --git a/backend/flowpilot/payload.go b/backend/flowpilot/payload.go index f6a7476ab..f3dd72edf 100644 --- a/backend/flowpilot/payload.go +++ b/backend/flowpilot/payload.go @@ -10,8 +10,3 @@ type Payload interface { func NewPayload() Payload { return jsonmanager.NewJSONManager() } - -// NewPayloadFromString creates a new instance of Payload with the given JSON data. -func NewPayloadFromString(data string) (Payload, error) { - return jsonmanager.NewJSONManagerFromString(data) -} diff --git a/backend/flowpilot/response.go b/backend/flowpilot/response.go index 0170e7bff..683d4c946 100644 --- a/backend/flowpilot/response.go +++ b/backend/flowpilot/response.go @@ -43,7 +43,7 @@ type PublicInput struct { // PublicResponse represents the response of an action execution. type PublicResponse struct { - State StateName `json:"state"` + StateName StateName `json:"state"` Status int `json:"status"` Payload interface{} `json:"payload,omitempty"` PublicActions PublicActions `json:"actions"` @@ -72,7 +72,7 @@ func newFlowResultFromError(stateName StateName, flowError FlowError, debug bool status := flowError.Status() publicResponse := PublicResponse{ - State: stateName, + StateName: stateName, Status: status, PublicError: &publicError, } @@ -98,8 +98,8 @@ type actionExecutionResult struct { // executionResult holds the result of an action execution. type executionResult struct { - nextState StateName - flowError FlowError + nextStateName StateName + flowError FlowError *actionExecutionResult } @@ -114,7 +114,7 @@ func (er *executionResult) generateResponse(fc defaultFlowContext, debug bool) F // Create the response object. resp := PublicResponse{ - State: er.nextState, + StateName: er.nextStateName, Status: http.StatusOK, Payload: payload, PublicActions: actions, @@ -137,10 +137,10 @@ func (er *executionResult) generateActions(fc defaultFlowContext) PublicActions var publicActions PublicActions // Get actions for the next addState. - detail, _ := fc.flow.getStateDetail(er.nextState) + state, _ := fc.flow.getState(er.nextStateName) - if detail != nil { - for _, action := range detail.actions { + if state != nil { + for _, action := range state.actions { actionName := action.GetName() actionDescription := action.GetDescription() @@ -155,7 +155,7 @@ func (er *executionResult) generateActions(fc defaultFlowContext) PublicActions } } - publicSchema := schema.toPublicSchema(er.nextState) + publicSchema := schema.toPublicSchema(er.nextStateName) // Create the action instance. publicAction := PublicAction{ diff --git a/backend/flowpilot/stash.go b/backend/flowpilot/stash.go index 37db0f0db..d757cdf1d 100644 --- a/backend/flowpilot/stash.go +++ b/backend/flowpilot/stash.go @@ -8,13 +8,13 @@ import ( // Stash defines the interface for managing JSON data. type Stash interface { - jsonmanager.JSONManager - - getLastStateFromHistory() (state, unscheduledState *StateName, numOfScheduledStates *int64, err error) - addStateToHistory(state StateName, unscheduledState *StateName, numOfScheduledStates *int64) error + getLastStateFromHistory() (stateName, unscheduledState *StateName, numOfScheduledStates *int64, err error) + addStateToHistory(stateName StateName, unscheduledStateName *StateName, numOfScheduledStates *int64) error removeLastStateFromHistory() error - addScheduledStates(scheduledStates ...StateName) error + addScheduledStates(scheduledStateNames ...StateName) error removeLastScheduledState() (*StateName, error) + + jsonmanager.JSONManager } // defaultStash implements the Stash interface. @@ -33,25 +33,25 @@ func NewStashFromString(data string) (Stash, error) { return &defaultStash{JSONManager: jm}, err } -// addStateToHistory adds a state to the history. Specify the values for unscheduledState and numOfScheduledStates to +// addStateToHistory adds a stateDetail to the history. Specify the values for unscheduledState and numOfScheduledStates to // maintain the list of scheduled states if sub-flows are involved. -func (s *defaultStash) addStateToHistory(state StateName, unscheduledState *StateName, numOfScheduledStates *int64) error { +func (s *defaultStash) addStateToHistory(stateName StateName, unscheduledStateName *StateName, numOfScheduledStates *int64) error { // Create a new JSONManager to manage the history item historyItem := jsonmanager.NewJSONManager() // Get the last state from history - lastState, _, _, err := s.getLastStateFromHistory() + lastStateName, _, _, err := s.getLastStateFromHistory() if err != nil { return err } - // If the last state is the same as the new state, do not add it again - if lastState != nil && *lastState == state { + // If the last state is the same as the current state, do not add it again + if lastStateName != nil && *lastStateName == stateName { return nil } - // Set the state in the history item - if err = historyItem.Set("s", state); err != nil { + // Set the stateDetail in the history item + if err = historyItem.Set("s", stateName); err != nil { return fmt.Errorf("failed to set state: %w", err) } @@ -62,14 +62,14 @@ func (s *defaultStash) addStateToHistory(state StateName, unscheduledState *Stat } } - // If unscheduledState is provided, set it in the history item - if unscheduledState != nil { - if err = historyItem.Set("u", *unscheduledState); err != nil { + // If unscheduledStateName is provided, set it in the history item + if unscheduledStateName != nil { + if err = historyItem.Set("u", *unscheduledStateName); err != nil { return fmt.Errorf("failed to set unscheduled_state: %w", err) } } - // Update the stashed history with the new history item + // Add the new history item to the history if err = s.Set("_.state_history.-1", historyItem.Unmarshal()); err != nil { return fmt.Errorf("failed to update stashed history: %w", err) } @@ -77,7 +77,7 @@ func (s *defaultStash) addStateToHistory(state StateName, unscheduledState *Stat return nil } -// removeLastStateFromHistory removes the last state from history. +// removeLastStateFromHistory removes the last stateDetail from history. func (s *defaultStash) removeLastStateFromHistory() error { if err := s.Delete("_.state_history.-1"); err != nil { return err @@ -86,10 +86,10 @@ func (s *defaultStash) removeLastStateFromHistory() error { return nil } -// getLastStateFromHistory returns the last state, as well as the values for unscheduledState and numOfScheduledStates. +// getLastStateFromHistory returns the last stateDetail, as well as the values for unscheduledState and numOfScheduledStates. // These values indicate that further states have been added or removed from the list of scheduled states during the -// last state. -func (s *defaultStash) getLastStateFromHistory() (state, unscheduledState *StateName, numOfScheduledStates *int64, err error) { +// last stateDetail. +func (s *defaultStash) getLastStateFromHistory() (stateName, unscheduledStateName *StateName, numOfScheduledStates *int64, err error) { // Get the index of the last history item lastItemPosition := s.Get("_.state_history.#").Int() - 1 @@ -111,15 +111,15 @@ func (s *defaultStash) getLastStateFromHistory() (state, unscheduledState *State return nil, nil, nil, errors.New("last history item is missing a value for 'state'") } - // Parse 's' field and assign it to the 'state' variable + // Parse 's' field and assign it to the 'stateDetail' variable sn := StateName(lastHistoryItem.Get("s").String()) - state = &sn + stateName = &sn // Check if 'u' field exists in the last history item if lastHistoryItem.Get("u").Exists() { // Parse 'u' field and assign it to the 'unscheduledState' variable usn := StateName(lastHistoryItem.Get("u").String()) - unscheduledState = &usn + unscheduledStateName = &usn } // Check if 'n' field exists in the last history item @@ -130,11 +130,11 @@ func (s *defaultStash) getLastStateFromHistory() (state, unscheduledState *State } // Return the parsed values - return state, unscheduledState, numOfScheduledStates, nil + return stateName, unscheduledStateName, numOfScheduledStates, nil } // addScheduledStates adds scheduled states. -func (s *defaultStash) addScheduledStates(scheduledStates ...StateName) error { +func (s *defaultStash) addScheduledStates(scheduledStateNames ...StateName) error { // get the current sub-flow stack from the stash stack := s.Get("_.scheduled_states").Array() @@ -145,7 +145,7 @@ func (s *defaultStash) addScheduledStates(scheduledStates ...StateName) error { } // prepend the scheduledStates to the list of previously defined scheduled states - newStack = append(scheduledStates, newStack...) + newStack = append(scheduledStateNames, newStack...) if err := s.Set("_.scheduled_states", newStack); err != nil { return fmt.Errorf("failed to set scheduled_states: %w", err) @@ -154,7 +154,7 @@ func (s *defaultStash) addScheduledStates(scheduledStates ...StateName) error { return nil } -// removeLastScheduledState removes and returns the last scheduled state if present. +// removeLastScheduledState removes and returns the last scheduled stateDetail if present. func (s *defaultStash) removeLastScheduledState() (*StateName, error) { // retrieve the previously scheduled states form the stash stack := s.Get("_.scheduled_states").Array() @@ -170,7 +170,7 @@ func (s *defaultStash) removeLastScheduledState() (*StateName, error) { } // get and remove first stack item - nextState := newStack[0] + nextStateName := newStack[0] newStack = newStack[1:] // stash the updated list of scheduled states @@ -178,5 +178,5 @@ func (s *defaultStash) removeLastScheduledState() (*StateName, error) { return nil, fmt.Errorf("failed to stash scheduled states while ending the sub-flow: %w", err) } - return &nextState, nil + return &nextStateName, nil } From 18bb9b86bf4cea855ec23ba90d910df12dd80ec4 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 19 Oct 2023 13:44:18 +0200 Subject: [PATCH 016/278] feat: introduce hook actions --- backend/flow_api_test/actions.go | 6 ++ backend/flow_api_test/flow.go | 1 + backend/flowpilot/builder.go | 92 ++++++++++++++---------- backend/flowpilot/context.go | 8 ++- backend/flowpilot/context_action_exec.go | 25 ++++++- backend/flowpilot/flow.go | 82 ++++++++++----------- 6 files changed, 131 insertions(+), 83 deletions(-) diff --git a/backend/flow_api_test/actions.go b/backend/flow_api_test/actions.go index 2f1ef7f0c..78138257d 100644 --- a/backend/flow_api_test/actions.go +++ b/backend/flow_api_test/actions.go @@ -378,3 +378,9 @@ func (m Back) Initialize(_ flowpilot.InitializationContext) {} func (m Back) Execute(c flowpilot.ExecutionContext) error { return c.ContinueToPreviousState() } + +type BeforeStateAction struct{} + +func (m BeforeStateAction) Execute(c flowpilot.HookExecutionContext) error { + return c.Payload().Set("before_action_executed", true) +} diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index 96077a80c..f4ce3b177 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -32,6 +32,7 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StatePasswordCreation, SubmitNewPassword{}). State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipPasskeyCreation{}). State(StateCreatePasskey, VerifyWAAssertion{}). + BeforeState(StateSuccess, BeforeStateAction{}). FixedStates(StateSignInOrSignUp, StateError, StateSuccess). SubFlows(FirstSubFlow, ThirdSubFlow). TTL(time.Minute * 10). diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 8200e8036..f3c50e51e 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -10,6 +10,7 @@ type FlowBuilder interface { TTL(ttl time.Duration) FlowBuilder State(stateName StateName, actions ...Action) FlowBuilder FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder + BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder Debug(enabled bool) FlowBuilder SubFlows(subFlows ...SubFlow) FlowBuilder Build() (Flow, error) @@ -18,6 +19,7 @@ type FlowBuilder interface { type SubFlowBuilder interface { State(stateName StateName, actions ...Action) SubFlowBuilder + BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder SubFlows(subFlows ...SubFlow) SubFlowBuilder Build() (SubFlow, error) MustBuild() SubFlow @@ -25,26 +27,27 @@ type SubFlowBuilder interface { // defaultFlowBuilderBase is the base flow builder struct. type defaultFlowBuilderBase struct { - flow StateTransitions - subFlows SubFlows - initialStateName StateName - stateDetails stateDetails + flow stateActions + subFlows SubFlows + stateDetails stateDetails + beforeHooks stateHooks } // defaultFlowBuilder is a builder struct for creating a new Flow. type defaultFlowBuilder struct { - path string - ttl time.Duration - errorStateName StateName - endStateName StateName - debug bool + path string + ttl time.Duration + initialStateName StateName + errorStateName StateName + endStateName StateName + debug bool defaultFlowBuilderBase } // newFlowBuilderBase creates a new defaultFlowBuilderBase instance. func newFlowBuilderBase() defaultFlowBuilderBase { - return defaultFlowBuilderBase{flow: make(StateTransitions), subFlows: make(SubFlows, 0), stateDetails: make(stateDetails)} + return defaultFlowBuilderBase{flow: make(stateActions), subFlows: make(SubFlows, 0), stateDetails: make(stateDetails), beforeHooks: make(stateHooks)} } // NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path. @@ -61,13 +64,12 @@ func (fb *defaultFlowBuilder) TTL(ttl time.Duration) FlowBuilder { } func (fb *defaultFlowBuilderBase) addState(stateName StateName, actions ...Action) { - var transitions Transitions - - for _, action := range actions { - transitions = append(transitions, Transition{Action: action}) - } + fb.flow[stateName] = append(fb.flow[stateName], actions...) +} - fb.flow[stateName] = transitions +func (fb *defaultFlowBuilderBase) addBeforeStateHooks(stateName StateName, hooks ...HookAction) { + fb.addDefaultStates(stateName) + fb.beforeHooks[stateName] = append(fb.beforeHooks[stateName], hooks...) } func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...SubFlow) { @@ -82,12 +84,17 @@ func (fb *defaultFlowBuilderBase) addDefaultStates(stateNames ...StateName) { } } -// State adds a new transition to the flow. +// 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 +} + // FixedStates sets the initial and final states of the flow. func (fb *defaultFlowBuilder) FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder { fb.initialStateName = initialStateName @@ -109,23 +116,25 @@ func (fb *defaultFlowBuilder) Debug(enabled bool) FlowBuilder { // 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 StateTransitions, subFlows SubFlows) error { +func (fb *defaultFlowBuilder) scanFlowStates(flow flowBase) error { // Iterate through states in the flow. - for stateName, transitions := range flow { + for stateName, actions := range flow.getFlow() { // Check if state name is already in use. if _, ok := fb.stateDetails[stateName]; ok { return fmt.Errorf("non-unique flow state '%s'", stateName) } - // Retrieve associated actions for the state. - actions := transitions.getActions() + sas := flow.getFlow() + sfs := flow.getSubFlows() + bhs := flow.getBeforeHooks() // Create state details. state := stateDetail{ - name: stateName, - flow: flow, - subFlows: subFlows, - actions: actions, + name: stateName, + actions: actions, + flow: sas, + subFlows: sfs, + beforeHooks: bhs[stateName], } // Store state details. @@ -133,8 +142,8 @@ func (fb *defaultFlowBuilder) scanFlowStates(flow StateTransitions, subFlows Sub } // Recursively scan sub-flows. - for _, sf := range subFlows { - if err := fb.scanFlowStates(sf.getFlow(), sf.getSubFlows()); err != nil { + for _, sf := range flow.getSubFlows() { + if err := fb.scanFlowStates(sf); err != nil { return err } } @@ -163,8 +172,8 @@ func (fb *defaultFlowBuilder) validate() error { if !fb.flow.stateExists(fb.endStateName) { return errors.New("fixed state 'endState' does not belong to the flow") } - if transitions, ok := fb.flow[fb.endStateName]; ok && len(transitions.getActions()) > 0 { - return fmt.Errorf("the specified endState '%s' is not allowed to have transitions", fb.endStateName) + if actions, ok := fb.flow[fb.endStateName]; ok && len(actions) > 0 { + return fmt.Errorf("the specified endState '%s' is not allowed to have actions", fb.endStateName) } return nil @@ -174,17 +183,14 @@ func (fb *defaultFlowBuilder) validate() error { func (fb *defaultFlowBuilder) Build() (Flow, error) { fb.addDefaultStates(fb.initialStateName, fb.errorStateName, fb.endStateName) - if err := fb.scanFlowStates(fb.flow, fb.subFlows); err != nil { - return nil, fmt.Errorf("failed to scan flow states: %w", err) - } - if err := fb.validate(); err != nil { return nil, fmt.Errorf("flow validation failed: %w", err) } - f := defaultFlow{ + flow := &defaultFlow{ path: fb.path, flow: fb.flow, + beforeHooks: fb.beforeHooks, initialStateName: fb.initialStateName, errorStateName: fb.errorStateName, endStateName: fb.endStateName, @@ -194,7 +200,11 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { debug: fb.debug, } - return &f, nil + if err := fb.scanFlowStates(flow); err != nil { + return nil, fmt.Errorf("failed to scan flow states: %w", err) + } + + return flow, nil } // MustBuild constructs and returns the Flow object, panics on error. @@ -224,18 +234,24 @@ func (sfb *defaultSubFlowBuilder) SubFlows(subFlows ...SubFlow) SubFlowBuilder { return sfb } -// State adds a new transition to the flow. +// State adds a new state to the flow. func (sfb *defaultSubFlowBuilder) State(stateName StateName, actions ...Action) SubFlowBuilder { sfb.addState(stateName, actions...) return sfb } +func (sfb *defaultSubFlowBuilder) BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder { + sfb.addBeforeStateHooks(stateName, hooks...) + return sfb +} + // Build constructs and returns the SubFlow object. func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { f := defaultFlow{ - flow: sfb.flow, - subFlows: sfb.subFlows, + flow: sfb.flow, + subFlows: sfb.subFlows, + beforeHooks: sfb.beforeHooks, } return &f, nil diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index a15db8f32..eb97a9e87 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -24,7 +24,7 @@ type flowContext interface { // GetCurrentState returns the current state of the flow. GetCurrentState() StateName // CurrentStateEquals returns true, when one of the given states matches the current state. - CurrentStateEquals(states ...StateName) bool + CurrentStateEquals(stateNames ...StateName) bool // GetPreviousState returns the previous state of the flow. GetPreviousState() (*StateName, error) // GetErrorState returns the designated error state of the flow. @@ -65,7 +65,7 @@ type actionExecutionContext interface { type actionExecutionContinuationContext interface { actionExecutionContext // ContinueFlow continues the flow execution to the specified next state. - ContinueFlow(nextState StateName) error + ContinueFlow(nextStateName StateName) error // ContinueFlowWithError continues the flow execution to the specified next state with an error. ContinueFlowWithError(nextStateName StateName, flowErr FlowError) error // StartSubFlow starts a sub-flow and continues the flow execution to the specified next states after the sub-flow has been ended. @@ -86,6 +86,10 @@ type ExecutionContext interface { actionExecutionContinuationContext } +type HookExecutionContext interface { + actionExecutionContext +} + // TODO: The following interfaces are meant for a plugin system. #tbd // PluginBeforeActionExecutionContext represents the context for a plugin before an action execution. diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 7872c1017..5d6a422b7 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -14,7 +14,7 @@ type defaultActionExecutionContext struct { defaultFlowContext // Embedding the defaultFlowContext for common context fields. } -// saveNextState updates the flow's state and saves Transition data after action execution. +// saveNextState updates the flow's state and stores data to the database. func (aec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { completed := executionResult.nextStateName == aec.flow.endStateName newVersion := aec.flowModel.Version + 1 @@ -39,7 +39,7 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio // Get the data to persists from the executed action schema for recording. inputDataToPersist := aec.input.getDataToPersist().String() - // Prepare parameters for creating a new Transition in the database. + // Prepare parameters for creating a new transition in the database. transitionCreation := transitionCreationParam{ flowID: aec.flowModel.ID, actionName: aec.actionName, @@ -91,6 +91,11 @@ func (aec *defaultActionExecutionContext) closeExecutionContext(nextStateName St return errors.New("execution context is closed already") } + err := aec.executeHookActions(nextStateName) + if err != nil { + return fmt.Errorf("error while executing hook actions: %w", err) + } + // Prepare the result for continuing the flow. actionResult := actionExecutionResult{ actionName: aec.actionName, @@ -113,6 +118,22 @@ func (aec *defaultActionExecutionContext) closeExecutionContext(nextStateName St return nil } +func (aec *defaultActionExecutionContext) executeHookActions(nextStateName StateName) error { + state, err := aec.flow.getState(nextStateName) + if err != nil { + return err + } + + for _, hook := range state.beforeHooks { + err = hook.Execute(aec) + if err != nil { + return err + } + } + + return nil +} + // Input returns the ExecutionSchema for accessing input data. func (aec *defaultActionExecutionContext) Input() ExecutionSchema { return aec.input diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index f6f665066..75c78c2f6 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -42,7 +42,7 @@ func WithInputData(inputData InputData) func(*flowExecutionOptions) { // StateName represents the name of a state in a flow. type StateName string -// ActionName represents the name of an action associated with a Transition. +// ActionName represents the name of an action. type ActionName string // Action defines the interface for flow actions. @@ -56,31 +56,21 @@ type Action interface { // Actions represents a list of Action type Actions []Action -// Transition holds an action associated with a state transition. -type Transition struct { - Action Action +// HookAction defines the interface for a hook action. +type HookAction interface { + Execute(HookExecutionContext) error } -// Transitions is a collection of Transition instances. -type Transitions []Transition +// HookActions represents a list of HookAction interfaces. +type HookActions []HookAction -// getActions returns the Actions associated with the transition. -func (ts *Transitions) getActions() Actions { - var actions Actions - - for _, t := range *ts { - actions = append(actions, t.Action) - } - - return actions -} - -// state represents details for a state, including the associated flow, available sub-flows and eligible actions. +// state represents details for a state, including the associated actions, available sub-flows and more. type stateDetail struct { - name StateName - flow StateTransitions - subFlows SubFlows - actions Actions + name StateName + flow stateActions + subFlows SubFlows + actions Actions + beforeHooks HookActions } // getAction returns the Action with the specified name. @@ -99,16 +89,19 @@ func (sd *stateDetail) getAction(actionName ActionName) (Action, error) { // stateDetails maps states to associated Actions, flows and sub-flows. type stateDetails map[StateName]*stateDetail -// StateTransitions maps states to associated Transitions. -type StateTransitions map[StateName]Transitions +// stateActions maps state names to associated actions. +type stateActions map[StateName]Actions + +// stateActions maps state names to associated hook actions. +type stateHooks map[StateName]HookActions // stateExists checks if a state exists in the flow. -func (st StateTransitions) stateExists(stateName StateName) bool { +func (st stateActions) stateExists(stateName StateName) bool { _, ok := st[stateName] return ok } -// SubFlows maps a sub-flow init state to StateTransitions. +// SubFlows represents a list of SubFlow interfaces. type SubFlows []SubFlow // stateExists checks if the given state exists in a sub-flow of the current flow. @@ -126,7 +119,8 @@ func (sfs SubFlows) stateExists(state StateName) bool { type flowBase interface { getState(stateName StateName) (*stateDetail, error) getSubFlows() SubFlows - getFlow() StateTransitions + getFlow() stateActions + getBeforeHooks() stateHooks } // Flow represents a flow. @@ -134,6 +128,7 @@ type Flow interface { Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) ResultFromError(err error) FlowResult setDefaults() + flowBase } @@ -142,20 +137,21 @@ type SubFlow interface { flowBase } -// defaultFlow defines a flow structure with states, transitions, and settings. +// defaultFlow defines a flow structure with states, actions, and settings. type defaultFlow struct { - flow StateTransitions // StateName transitions mapping. - subFlows SubFlows // The sub-flows of the current flow. - stateDetails stateDetails // Maps state names to flow details. - path string // flow path or identifier. - initialStateName StateName // Initial state of the flow. - errorStateName StateName // State representing errors. - endStateName StateName // Final state of the flow. - ttl time.Duration // Time-to-live for the flow. - debug bool // Enables debug mode. -} - -// getActionsForState returns transitions for a specified state. + flow stateActions // StateName to Actions mapping. + subFlows SubFlows // The sub-flows of the current flow. + stateDetails stateDetails // Maps state names to flow details. + path string // flow path or identifier. + beforeHooks stateHooks // StateName to HookActions mapping. + initialStateName StateName // Initial state of the flow. + errorStateName StateName // State representing errors. + endStateName StateName // Final state of the flow. + ttl time.Duration // Time-to-live for the flow. + debug bool // Enables debug mode. +} + +// getActionsForState returns state details for the specified state. func (f *defaultFlow) getState(stateName StateName) (*stateDetail, error) { if state, ok := f.stateDetails[stateName]; ok { return state, nil @@ -170,10 +166,14 @@ func (f *defaultFlow) getSubFlows() SubFlows { } // getFlow returns the state to action mapping of the current flow. -func (f *defaultFlow) getFlow() StateTransitions { +func (f *defaultFlow) getFlow() stateActions { return f.flow } +func (f *defaultFlow) getBeforeHooks() stateHooks { + return f.beforeHooks +} + // setDefaults sets default values for defaultFlow settings. func (f *defaultFlow) setDefaults() { if f.ttl.Seconds() == 0 { From 73a33998e847cab3c851054e9e346cfb80c0aa5e Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Thu, 19 Oct 2023 18:17:28 +0200 Subject: [PATCH 017/278] feat: flow builder interface improved --- backend/flow_api_test/actions.go | 6 + backend/flow_api_test/flow.go | 4 +- backend/flowpilot/builder.go | 179 +++++++----------- backend/flowpilot/builder_subflow.go | 66 +++++++ backend/flowpilot/context.go | 2 - backend/flowpilot/context_action_exec.go | 28 ++- backend/flowpilot/context_flow.go | 5 - backend/flowpilot/db.go | 4 - backend/flowpilot/flow.go | 28 ++- .../20230810173315_create_flows.up.fizz | 1 - backend/persistence/models/flow.go | 2 - backend/persistence/models/flowdb.go | 4 +- 12 files changed, 183 insertions(+), 146 deletions(-) create mode 100644 backend/flowpilot/builder_subflow.go diff --git a/backend/flow_api_test/actions.go b/backend/flow_api_test/actions.go index 78138257d..664129b05 100644 --- a/backend/flow_api_test/actions.go +++ b/backend/flow_api_test/actions.go @@ -384,3 +384,9 @@ type BeforeStateAction struct{} func (m BeforeStateAction) Execute(c flowpilot.HookExecutionContext) error { return c.Payload().Set("before_action_executed", true) } + +type AfterStateAction struct{} + +func (m AfterStateAction) Execute(c flowpilot.HookExecutionContext) error { + return c.Payload().Set("after_action_executed", true) +} diff --git a/backend/flow_api_test/flow.go b/backend/flow_api_test/flow.go index f4ce3b177..4cca099de 100644 --- a/backend/flow_api_test/flow.go +++ b/backend/flow_api_test/flow.go @@ -12,6 +12,7 @@ var ThirdSubFlow = flowpilot.NewSubFlow(). var SecondSubFlow = flowpilot.NewSubFlow(). State(StateSecondSubFlowInit, ContinueToFinal{}, Back{}). State(StateSecondSubFlowFinal, EndSubFlow{}, Back{}). + AfterState(StateSecondSubFlowFinal, AfterStateAction{}). MustBuild() var FirstSubFlow = flowpilot.NewSubFlow(). @@ -33,7 +34,8 @@ var Flow = flowpilot.NewFlow("/flow_api_login"). State(StateConfirmPasskeyCreation, GetWAAssertion{}, SkipPasskeyCreation{}). State(StateCreatePasskey, VerifyWAAssertion{}). BeforeState(StateSuccess, BeforeStateAction{}). - FixedStates(StateSignInOrSignUp, StateError, StateSuccess). + InitialState(StateSignInOrSignUp). + ErrorState(StateError). SubFlows(FirstSubFlow, ThirdSubFlow). TTL(time.Minute * 10). Debug(true). diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index f3c50e51e..2d972627f 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -9,28 +9,23 @@ import ( type FlowBuilder interface { TTL(ttl time.Duration) FlowBuilder State(stateName StateName, actions ...Action) FlowBuilder - FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder + InitialState(stateName StateName) FlowBuilder + ErrorState(stateName StateName) FlowBuilder BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder + AfterState(stateName StateName, hooks ...HookAction) FlowBuilder Debug(enabled bool) FlowBuilder SubFlows(subFlows ...SubFlow) FlowBuilder Build() (Flow, error) MustBuild() Flow } -type SubFlowBuilder interface { - State(stateName StateName, actions ...Action) SubFlowBuilder - BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder - SubFlows(subFlows ...SubFlow) SubFlowBuilder - Build() (SubFlow, error) - MustBuild() SubFlow -} - // defaultFlowBuilderBase is the base flow builder struct. type defaultFlowBuilderBase struct { flow stateActions subFlows SubFlows stateDetails stateDetails beforeHooks stateHooks + afterHooks stateHooks } // defaultFlowBuilder is a builder struct for creating a new Flow. @@ -39,7 +34,6 @@ type defaultFlowBuilder struct { ttl time.Duration initialStateName StateName errorStateName StateName - endStateName StateName debug bool defaultFlowBuilderBase @@ -47,7 +41,13 @@ type defaultFlowBuilder struct { // newFlowBuilderBase creates a new defaultFlowBuilderBase instance. func newFlowBuilderBase() defaultFlowBuilderBase { - return defaultFlowBuilderBase{flow: make(stateActions), subFlows: make(SubFlows, 0), stateDetails: make(stateDetails), beforeHooks: make(stateHooks)} + return defaultFlowBuilderBase{ + flow: make(stateActions), + subFlows: make(SubFlows, 0), + stateDetails: make(stateDetails), + beforeHooks: make(stateHooks), + afterHooks: make(stateHooks), + } } // NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path. @@ -68,15 +68,20 @@ func (fb *defaultFlowBuilderBase) addState(stateName StateName, actions ...Actio } func (fb *defaultFlowBuilderBase) addBeforeStateHooks(stateName StateName, hooks ...HookAction) { - fb.addDefaultStates(stateName) + fb.addStateIfNotExists(stateName) fb.beforeHooks[stateName] = append(fb.beforeHooks[stateName], hooks...) } +func (fb *defaultFlowBuilderBase) addAfterStateHooks(stateName StateName, hooks ...HookAction) { + fb.addStateIfNotExists(stateName) + fb.afterHooks[stateName] = append(fb.afterHooks[stateName], hooks...) +} + func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...SubFlow) { fb.subFlows = append(fb.subFlows, subFlows...) } -func (fb *defaultFlowBuilderBase) addDefaultStates(stateNames ...StateName) { +func (fb *defaultFlowBuilderBase) addStateIfNotExists(stateNames ...StateName) { for _, stateName := range stateNames { if _, exists := fb.flow[stateName]; !exists { fb.addState(stateName) @@ -84,36 +89,6 @@ func (fb *defaultFlowBuilderBase) addDefaultStates(stateNames ...StateName) { } } -// 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 -} - -// FixedStates sets the initial and final states of the flow. -func (fb *defaultFlowBuilder) FixedStates(initialStateName, errorStateName, finalStateName StateName) FlowBuilder { - fb.initialStateName = initialStateName - fb.errorStateName = errorStateName - fb.endStateName = finalStateName - 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 -} - // 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 { @@ -124,17 +99,19 @@ func (fb *defaultFlowBuilder) scanFlowStates(flow flowBase) error { return fmt.Errorf("non-unique flow state '%s'", stateName) } - sas := flow.getFlow() + f := flow.getFlow() sfs := flow.getSubFlows() bhs := flow.getBeforeHooks() + ahs := flow.getAfterHooks() // Create state details. state := stateDetail{ name: stateName, actions: actions, - flow: sas, + flow: f, subFlows: sfs, beforeHooks: bhs[stateName], + afterHooks: ahs[stateName], } // Store state details. @@ -160,44 +137,76 @@ func (fb *defaultFlowBuilder) validate() error { if len(fb.errorStateName) == 0 { return errors.New("fixed state 'errorState' is not set") } - if len(fb.endStateName) == 0 { - return errors.New("fixed state 'endState' is not set") - } if !fb.flow.stateExists(fb.initialStateName) { return errors.New("fixed state 'initialState' does not belong to the flow") } if !fb.flow.stateExists(fb.errorStateName) { return errors.New("fixed state 'errorState' does not belong to the flow") } - if !fb.flow.stateExists(fb.endStateName) { - return errors.New("fixed state 'endState' does not belong to the flow") - } - if actions, ok := fb.flow[fb.endStateName]; ok && len(actions) > 0 { - return fmt.Errorf("the specified endState '%s' is not allowed to have actions", fb.endStateName) - } 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) InitialState(stateName StateName) FlowBuilder { + fb.addStateIfNotExists(stateName) + fb.initialStateName = stateName + 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) { - fb.addDefaultStates(fb.initialStateName, fb.errorStateName, fb.endStateName) - if err := fb.validate(); err != nil { return nil, fmt.Errorf("flow validation failed: %w", err) } + dfb := defaultFlowBase{ + flow: fb.flow, + subFlows: fb.subFlows, + beforeHooks: fb.beforeHooks, + afterHooks: fb.afterHooks, + } + flow := &defaultFlow{ path: fb.path, - flow: fb.flow, - beforeHooks: fb.beforeHooks, initialStateName: fb.initialStateName, errorStateName: fb.errorStateName, - endStateName: fb.endStateName, - subFlows: fb.subFlows, stateDetails: fb.stateDetails, ttl: fb.ttl, debug: fb.debug, + defaultFlowBase: dfb, } if err := fb.scanFlowStates(flow); err != nil { @@ -217,53 +226,3 @@ func (fb *defaultFlowBuilder) MustBuild() Flow { return f } - -// defaultFlowBuilder is a builder struct for creating a new SubFlow. -type defaultSubFlowBuilder struct { - defaultFlowBuilderBase -} - -// NewSubFlow creates a new SubFlowBuilder. -func NewSubFlow() SubFlowBuilder { - fbBase := newFlowBuilderBase() - return &defaultSubFlowBuilder{defaultFlowBuilderBase: fbBase} -} - -func (sfb *defaultSubFlowBuilder) SubFlows(subFlows ...SubFlow) SubFlowBuilder { - sfb.addSubFlows(subFlows...) - return sfb -} - -// State adds a new state to the flow. -func (sfb *defaultSubFlowBuilder) State(stateName StateName, actions ...Action) SubFlowBuilder { - sfb.addState(stateName, actions...) - return sfb -} - -func (sfb *defaultSubFlowBuilder) BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder { - sfb.addBeforeStateHooks(stateName, hooks...) - return sfb -} - -// Build constructs and returns the SubFlow object. -func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { - - f := defaultFlow{ - flow: sfb.flow, - subFlows: sfb.subFlows, - beforeHooks: sfb.beforeHooks, - } - - return &f, nil -} - -// MustBuild constructs and returns the SubFlow object, panics on error. -func (sfb *defaultSubFlowBuilder) MustBuild() SubFlow { - sf, err := sfb.Build() - - if err != nil { - panic(err) - } - - return sf -} diff --git a/backend/flowpilot/builder_subflow.go b/backend/flowpilot/builder_subflow.go new file mode 100644 index 000000000..048549b33 --- /dev/null +++ b/backend/flowpilot/builder_subflow.go @@ -0,0 +1,66 @@ +package flowpilot + +type SubFlowBuilder interface { + State(stateName StateName, actions ...Action) SubFlowBuilder + BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder + AfterState(stateName StateName, hooks ...HookAction) SubFlowBuilder + SubFlows(subFlows ...SubFlow) SubFlowBuilder + Build() (SubFlow, error) + MustBuild() SubFlow +} + +// defaultFlowBuilder is a builder struct for creating a new SubFlow. +type defaultSubFlowBuilder struct { + defaultFlowBuilderBase +} + +// NewSubFlow creates a new SubFlowBuilder. +func NewSubFlow() SubFlowBuilder { + fbBase := newFlowBuilderBase() + return &defaultSubFlowBuilder{defaultFlowBuilderBase: fbBase} +} + +func (sfb *defaultSubFlowBuilder) SubFlows(subFlows ...SubFlow) SubFlowBuilder { + sfb.addSubFlows(subFlows...) + return sfb +} + +// State adds a new state to the flow. +func (sfb *defaultSubFlowBuilder) State(stateName StateName, actions ...Action) SubFlowBuilder { + sfb.addState(stateName, actions...) + return sfb +} + +func (sfb *defaultSubFlowBuilder) BeforeState(stateName StateName, hooks ...HookAction) SubFlowBuilder { + sfb.addBeforeStateHooks(stateName, hooks...) + return sfb +} + +func (sfb *defaultSubFlowBuilder) AfterState(stateName StateName, hooks ...HookAction) SubFlowBuilder { + sfb.addAfterStateHooks(stateName, hooks...) + return sfb +} + +// Build constructs and returns the SubFlow object. +func (sfb *defaultSubFlowBuilder) Build() (SubFlow, error) { + + f := defaultFlowBase{ + flow: sfb.flow, + subFlows: sfb.subFlows, + beforeHooks: sfb.beforeHooks, + afterHooks: sfb.afterHooks, + } + + return &f, nil +} + +// MustBuild constructs and returns the SubFlow object, panics on error. +func (sfb *defaultSubFlowBuilder) MustBuild() SubFlow { + sf, err := sfb.Build() + + if err != nil { + panic(err) + } + + return sf +} diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index eb97a9e87..937eab3f1 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -29,8 +29,6 @@ type flowContext interface { GetPreviousState() (*StateName, error) // GetErrorState returns the designated error state of the flow. GetErrorState() StateName - // GetEndState returns the final state of the flow. - GetEndState() StateName // StateExists checks if a given state exists within the flow. StateExists(stateName StateName) bool } diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 5d6a422b7..8e9a38f77 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -16,7 +16,6 @@ type defaultActionExecutionContext struct { // saveNextState updates the flow's state and stores data to the database. func (aec *defaultActionExecutionContext) saveNextState(executionResult executionResult) error { - completed := executionResult.nextStateName == aec.flow.endStateName newVersion := aec.flowModel.Version + 1 stashData := aec.stash.String() @@ -26,7 +25,6 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio nextState: executionResult.nextStateName, stashData: stashData, version: newVersion, - completed: completed, expiresAt: aec.flowModel.ExpiresAt, createdAt: aec.flowModel.CreatedAt, } @@ -68,9 +66,7 @@ func (aec *defaultActionExecutionContext) continueFlow(nextStateName StateName, nextStateAllowed := currentState.flow.stateExists(nextStateName) // Check if the specified nextStateName is valid. - if !(nextStateAllowed || - nextStateName == aec.flow.endStateName || - nextStateName == aec.flow.errorStateName) { + if !(nextStateAllowed || nextStateName == aec.flow.errorStateName) { return fmt.Errorf("progression to the specified state '%s' is not allowed", nextStateName) } @@ -119,15 +115,27 @@ func (aec *defaultActionExecutionContext) closeExecutionContext(nextStateName St } func (aec *defaultActionExecutionContext) executeHookActions(nextStateName StateName) error { - state, err := aec.flow.getState(nextStateName) + currentState, err := aec.flow.getState(aec.flowModel.CurrentState) if err != nil { return err } - for _, hook := range state.beforeHooks { + for _, hook := range currentState.afterHooks { err = hook.Execute(aec) if err != nil { - return err + return fmt.Errorf("failed to execute hook action after state: %w", err) + } + } + + nextState, err := aec.flow.getState(nextStateName) + if err != nil { + return err + } + + for _, hook := range nextState.beforeHooks { + err = hook.Execute(aec) + if err != nil { + return fmt.Errorf("failed to execute hook action before state: %w", err) } } @@ -229,7 +237,7 @@ func (aec *defaultActionExecutionContext) StartSubFlow(entryStateName StateName, var scheduledStates []StateName - // Validate the specified nextStates and append valid states to the list of scheduledStates. + // Append valid states to the list of scheduledStates. for index, nextStateName := range nextStateNames { stateExists := currentState.flow.stateExists(nextStateName) subFlowStateExists := currentState.subFlows.stateExists(nextStateName) @@ -282,7 +290,7 @@ func (aec *defaultActionExecutionContext) EndSubFlow() error { // If no scheduled state is available, set it to the end state. if scheduledStateName == nil { - scheduledStateName = &aec.flow.endStateName + return ErrorFlowDiscontinuity.Wrap(errors.New("can't progress the flow, because no scheduled states were available after the sub-flow ended")) } else { // Add the current state to the execution history. err = aec.stash.addStateToHistory(currentStateName, scheduledStateName, nil) diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index 1f3a9b6f9..ce258e056 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -56,11 +56,6 @@ func (fc *defaultFlowContext) GetErrorState() StateName { return fc.flow.errorStateName } -// GetEndState returns the final state of the flow. -func (fc *defaultFlowContext) GetEndState() StateName { - return fc.flow.endStateName -} - // Stash returns the JSONManager for accessing stash data. func (fc *defaultFlowContext) Stash() Stash { return fc.stash diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go index 54a1439dc..54e0899c6 100644 --- a/backend/flowpilot/db.go +++ b/backend/flowpilot/db.go @@ -12,7 +12,6 @@ type FlowModel struct { CurrentState StateName // Current addState of the defaultFlow. StashData string // Stash data associated with the defaultFlow. Version int // Version of the defaultFlow. - Completed bool // Flag indicating if the defaultFlow is completed. ExpiresAt time.Time // Expiry time of the defaultFlow. CreatedAt time.Time // Creation time of the defaultFlow. UpdatedAt time.Time // Update time of the defaultFlow. @@ -81,7 +80,6 @@ func (w *DefaultFlowDBWrapper) CreateFlowWithParam(p flowCreationParam) (*FlowMo CurrentState: p.currentState, StashData: "{}", Version: 0, - Completed: false, ExpiresAt: p.expiresAt, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), @@ -102,7 +100,6 @@ type flowUpdateParam struct { nextState StateName // Next addState of the flow. stashData string // Updated stash data for the flow. version int // Updated version of the flow. - completed bool // Flag indicating if the flow is completed. expiresAt time.Time // Updated expiry time of the flow. createdAt time.Time // Original creation time of the flow. } @@ -115,7 +112,6 @@ func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowMode CurrentState: p.nextState, StashData: p.stashData, Version: p.version, - Completed: p.completed, ExpiresAt: p.expiresAt, UpdatedAt: time.Now().UTC(), CreatedAt: p.createdAt, diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index 75c78c2f6..a48a7eae4 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -71,6 +71,7 @@ type stateDetail struct { subFlows SubFlows actions Actions beforeHooks HookActions + afterHooks HookActions } // getAction returns the Action with the specified name. @@ -117,17 +118,19 @@ func (sfs SubFlows) stateExists(state StateName) bool { // flowBase represents the base of the flow interfaces. type flowBase interface { - getState(stateName StateName) (*stateDetail, error) getSubFlows() SubFlows getFlow() stateActions getBeforeHooks() stateHooks + getAfterHooks() stateHooks } // Flow represents a flow. type Flow interface { Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) ResultFromError(err error) FlowResult + setDefaults() + getState(stateName StateName) (*stateDetail, error) flowBase } @@ -137,18 +140,23 @@ type SubFlow interface { flowBase } +type defaultFlowBase struct { + flow stateActions // StateName to Actions mapping. + subFlows SubFlows // The sub-flows of the current flow. + beforeHooks stateHooks // StateName to HookActions mapping. + afterHooks stateHooks // StateName to HookActions mapping. +} + // defaultFlow defines a flow structure with states, actions, and settings. type defaultFlow struct { - flow stateActions // StateName to Actions mapping. - subFlows SubFlows // The sub-flows of the current flow. stateDetails stateDetails // Maps state names to flow details. path string // flow path or identifier. - beforeHooks stateHooks // StateName to HookActions mapping. initialStateName StateName // Initial state of the flow. errorStateName StateName // State representing errors. - endStateName StateName // Final state of the flow. ttl time.Duration // Time-to-live for the flow. debug bool // Enables debug mode. + + defaultFlowBase } // getActionsForState returns state details for the specified state. @@ -161,19 +169,23 @@ func (f *defaultFlow) getState(stateName StateName) (*stateDetail, error) { } // getSubFlows returns the sub-flows of the current flow. -func (f *defaultFlow) getSubFlows() SubFlows { +func (f *defaultFlowBase) getSubFlows() SubFlows { return f.subFlows } // getFlow returns the state to action mapping of the current flow. -func (f *defaultFlow) getFlow() stateActions { +func (f *defaultFlowBase) getFlow() stateActions { return f.flow } -func (f *defaultFlow) getBeforeHooks() stateHooks { +func (f *defaultFlowBase) getBeforeHooks() stateHooks { return f.beforeHooks } +func (f *defaultFlowBase) getAfterHooks() stateHooks { + return f.afterHooks +} + // setDefaults sets default values for defaultFlow settings. func (f *defaultFlow) setDefaults() { if f.ttl.Seconds() == 0 { diff --git a/backend/persistence/migrations/20230810173315_create_flows.up.fizz b/backend/persistence/migrations/20230810173315_create_flows.up.fizz index f612b077d..4efb791d5 100644 --- a/backend/persistence/migrations/20230810173315_create_flows.up.fizz +++ b/backend/persistence/migrations/20230810173315_create_flows.up.fizz @@ -3,7 +3,6 @@ create_table("flows") { t.Column("current_state", "string") t.Column("stash_data", "string", {"size": 4096}) t.Column("version", "int") - t.Column("completed", "bool", {"default": false}) t.Column("expires_at", "timestamp") t.Timestamps() } diff --git a/backend/persistence/models/flow.go b/backend/persistence/models/flow.go index a5f50faba..63947bc4a 100644 --- a/backend/persistence/models/flow.go +++ b/backend/persistence/models/flow.go @@ -15,7 +15,6 @@ type Flow struct { CurrentState string `json:"current_state" db:"current_state"` StashData string `json:"stash_data" db:"stash_data"` Version int `json:"version" db:"version"` - Completed bool `json:"completed" db:"completed"` ExpiresAt time.Time `json:"expires_at" db:"expires_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` @@ -28,7 +27,6 @@ func (f *Flow) ToFlowpilotModel() *flowpilot.FlowModel { CurrentState: flowpilot.StateName(f.CurrentState), StashData: f.StashData, Version: f.Version, - Completed: f.Completed, ExpiresAt: f.ExpiresAt, CreatedAt: f.CreatedAt, UpdatedAt: f.UpdatedAt, diff --git a/backend/persistence/models/flowdb.go b/backend/persistence/models/flowdb.go index d532fd5d6..d8f2c9f7e 100644 --- a/backend/persistence/models/flowdb.go +++ b/backend/persistence/models/flowdb.go @@ -91,7 +91,6 @@ func (flowDB FlowDB) CreateFlow(flowModel flowpilot.FlowModel) error { CurrentState: string(flowModel.CurrentState), StashData: flowModel.StashData, Version: flowModel.Version, - Completed: flowModel.Completed, ExpiresAt: flowModel.ExpiresAt, CreatedAt: flowModel.CreatedAt, UpdatedAt: flowModel.UpdatedAt, @@ -111,7 +110,6 @@ func (flowDB FlowDB) UpdateFlow(flowModel flowpilot.FlowModel) error { CurrentState: string(flowModel.CurrentState), StashData: flowModel.StashData, Version: flowModel.Version, - Completed: flowModel.Completed, ExpiresAt: flowModel.ExpiresAt, CreatedAt: flowModel.CreatedAt, UpdatedAt: flowModel.UpdatedAt, @@ -122,7 +120,7 @@ func (flowDB FlowDB) UpdateFlow(flowModel flowpilot.FlowModel) error { count, err := flowDB.tx. Where("id = ?", f.ID). Where("version = ?", previousVersion). - UpdateQuery(f, "current_state", "stash_data", "version", "completed") + UpdateQuery(f, "current_state", "stash_data", "version") if err != nil { return err } From 87c8de4c89ea845f8f2c4c4542eb65445a685678 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Wed, 25 Oct 2023 19:00:09 +0200 Subject: [PATCH 018/278] feat: adds the possibility to set context values to the flow --- backend/flowpilot/builder.go | 1 + backend/flowpilot/context.go | 4 +++- backend/flowpilot/context_action_exec.go | 4 ++-- backend/flowpilot/context_flow.go | 7 ++++++- backend/flowpilot/db.go | 26 ++++++++++++------------ backend/flowpilot/flow.go | 17 ++++++++++++++-- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/backend/flowpilot/builder.go b/backend/flowpilot/builder.go index 2d972627f..92f8b148d 100644 --- a/backend/flowpilot/builder.go +++ b/backend/flowpilot/builder.go @@ -207,6 +207,7 @@ func (fb *defaultFlowBuilder) Build() (Flow, error) { ttl: fb.ttl, debug: fb.debug, defaultFlowBase: dfb, + contextValues: make(contextValues), } if err := fb.scanFlowStates(flow); err != nil { diff --git a/backend/flowpilot/context.go b/backend/flowpilot/context.go index 937eab3f1..f5a11a0de 100644 --- a/backend/flowpilot/context.go +++ b/backend/flowpilot/context.go @@ -11,6 +11,8 @@ import ( // flowContext represents the basic context for a flow. type flowContext interface { + // Get returns the context value with the given name. + Get(string) interface{} // GetFlowID returns the unique ID of the current defaultFlow. GetFlowID() uuid.UUID // GetPath returns the current path within the flow. @@ -113,7 +115,7 @@ func createAndInitializeFlow(db FlowDB, flow defaultFlow) (FlowResult, error) { // Create a new flow model with the provided parameters. flowCreation := flowCreationParam{currentState: flow.initialStateName, expiresAt: expiresAt} - flowModel, err := dbw.CreateFlowWithParam(flowCreation) + flowModel, err := dbw.createFlowWithParam(flowCreation) if err != nil { return nil, fmt.Errorf("failed to create flow: %w", err) } diff --git a/backend/flowpilot/context_action_exec.go b/backend/flowpilot/context_action_exec.go index 8e9a38f77..8fb0ec6c2 100644 --- a/backend/flowpilot/context_action_exec.go +++ b/backend/flowpilot/context_action_exec.go @@ -30,7 +30,7 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio } // Update the flow model in the database. - if _, err := aec.dbw.UpdateFlowWithParam(flowUpdate); err != nil { + if _, err := aec.dbw.updateFlowWithParam(flowUpdate); err != nil { return fmt.Errorf("failed to store updated flow: %w", err) } @@ -48,7 +48,7 @@ func (aec *defaultActionExecutionContext) saveNextState(executionResult executio } // Create a new Transition in the database. - if _, err := aec.dbw.CreateTransitionWithParam(transitionCreation); err != nil { + if _, err := aec.dbw.createTransitionWithParam(transitionCreation); err != nil { return fmt.Errorf("failed to store a new transition: %w", err) } diff --git a/backend/flowpilot/context_flow.go b/backend/flowpilot/context_flow.go index ce258e056..a1702be42 100644 --- a/backend/flowpilot/context_flow.go +++ b/backend/flowpilot/context_flow.go @@ -10,7 +10,7 @@ type defaultFlowContext struct { payload Payload // JSONManager for payload data. stash Stash // JSONManager for stash data. flow defaultFlow // The associated defaultFlow instance. - dbw FlowDBWrapper // Wrapped FlowDB instance with additional functionality. + dbw flowDBWrapper // Wrapped FlowDB instance with additional functionality. flowModel FlowModel // The current FlowModel. } @@ -72,6 +72,11 @@ func (fc *defaultFlowContext) StateExists(stateName StateName) bool { return false } +// Get returns the context value with the given name. +func (fc *defaultFlowContext) Get(name string) interface{} { + return fc.flow.contextValues[name] +} + // FetchActionInput fetches input data for a specific action. func (fc *defaultFlowContext) FetchActionInput(methodName ActionName) (ReadOnlyActionInput, error) { // Find the last Transition with the specified method from the database wrapper. diff --git a/backend/flowpilot/db.go b/backend/flowpilot/db.go index 54e0899c6..25b6d3299 100644 --- a/backend/flowpilot/db.go +++ b/backend/flowpilot/db.go @@ -42,22 +42,22 @@ type FlowDB interface { FindLastTransitionWithAction(flowID uuid.UUID, method ActionName) (*TransitionModel, error) } -// FlowDBWrapper is an extended FlowDB interface that includes additional methods. -type FlowDBWrapper interface { +// flowDBWrapper is an extended FlowDB interface that includes additional methods. +type flowDBWrapper interface { FlowDB - CreateFlowWithParam(p flowCreationParam) (*FlowModel, error) - UpdateFlowWithParam(p flowUpdateParam) (*FlowModel, error) - CreateTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) + createFlowWithParam(p flowCreationParam) (*FlowModel, error) + updateFlowWithParam(p flowUpdateParam) (*FlowModel, error) + createTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) } -// DefaultFlowDBWrapper wraps a FlowDB instance to provide additional functionality. -type DefaultFlowDBWrapper struct { +// defaultFlowDBWrapper wraps a FlowDB instance to provide additional functionality. +type defaultFlowDBWrapper struct { FlowDB } -// wrapDB wraps a FlowDB instance to provide FlowDBWrapper functionality. -func wrapDB(db FlowDB) FlowDBWrapper { - return &DefaultFlowDBWrapper{FlowDB: db} +// wrapDB wraps a FlowDB instance to provide flowDBWrapper functionality. +func wrapDB(db FlowDB) flowDBWrapper { + return &defaultFlowDBWrapper{FlowDB: db} } // flowCreationParam holds parameters for creating a new defaultFlow. @@ -67,7 +67,7 @@ type flowCreationParam struct { } // CreateFlowWithParam creates a new defaultFlow with the given parameters. -func (w *DefaultFlowDBWrapper) CreateFlowWithParam(p flowCreationParam) (*FlowModel, error) { +func (w *defaultFlowDBWrapper) createFlowWithParam(p flowCreationParam) (*FlowModel, error) { // Generate a new UUID for the defaultFlow. flowID, err := uuid.NewV4() if err != nil { @@ -105,7 +105,7 @@ type flowUpdateParam struct { } // UpdateFlowWithParam updates the specified defaultFlow with the given parameters. -func (w *DefaultFlowDBWrapper) UpdateFlowWithParam(p flowUpdateParam) (*FlowModel, error) { +func (w *defaultFlowDBWrapper) updateFlowWithParam(p flowUpdateParam) (*FlowModel, error) { // Prepare the updated FlowModel. fm := FlowModel{ ID: p.flowID, @@ -137,7 +137,7 @@ type transitionCreationParam struct { } // CreateTransitionWithParam creates a new Transition with the given parameters. -func (w *DefaultFlowDBWrapper) CreateTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) { +func (w *defaultFlowDBWrapper) createTransitionWithParam(p transitionCreationParam) (*TransitionModel, error) { // Generate a new UUID for the Transition. transitionID, err := uuid.NewV4() if err != nil { diff --git a/backend/flowpilot/flow.go b/backend/flowpilot/flow.go index a48a7eae4..bfc5d06d5 100644 --- a/backend/flowpilot/flow.go +++ b/backend/flowpilot/flow.go @@ -126,12 +126,18 @@ type flowBase interface { // Flow represents a flow. type Flow interface { + // Execute executes the flow using the provided FlowDB and options. + // It returns the result of the flow execution and an error if any. Execute(db FlowDB, opts ...func(*flowExecutionOptions)) (FlowResult, error) + // ResultFromError converts an error into a FlowResult. ResultFromError(err error) FlowResult - + // Set sets a value with the given key in the flow context. + Set(string, interface{}) + // setDefaults sets the default values for the flow. setDefaults() + // getState retrieves the details of a specific state in the flow. getState(stateName StateName) (*stateDetail, error) - + // Embed the flowBase interface. flowBase } @@ -140,6 +146,8 @@ type SubFlow interface { flowBase } +type contextValues map[string]interface{} + type defaultFlowBase struct { flow stateActions // StateName to Actions mapping. subFlows SubFlows // The sub-flows of the current flow. @@ -155,10 +163,15 @@ type defaultFlow struct { errorStateName StateName // State representing errors. ttl time.Duration // Time-to-live for the flow. debug bool // Enables debug mode. + contextValues contextValues // Values to be used within the flow context. defaultFlowBase } +func (f *defaultFlow) Set(name string, value interface{}) { + f.contextValues[name] = value +} + // getActionsForState returns state details for the specified state. func (f *defaultFlow) getState(stateName StateName) (*stateDetail, error) { if state, ok := f.stateDetails[stateName]; ok { From da197cf2a18312b5c4434bab8ecd87c83a177cd8 Mon Sep 17 00:00:00 2001 From: lfleischmann <67686424+lfleischmann@users.noreply.github.com> Date: Thu, 26 Oct 2023 14:01:46 +0200 Subject: [PATCH 019/278] Feat first action implementations --- backend/Dockerfile | 1 + backend/config/config.go | 42 +++ backend/config/config.yaml | 21 ++ .../actions/continue.go | 34 ++ .../actions/generate_recovery_codes.go | 35 ++ .../actions/get_wa_creation_options.go | 129 +++++++ .../actions/get_wa_request_options.go | 34 ++ .../actions/login_with_oauth.go | 38 ++ .../actions/send_capabilities.go | 66 ++++ .../actions/send_capabilities_test.go | 112 ++++++ .../actions/send_wa_assertion_response.go | 35 ++ .../actions/send_wa_attestation_response.go | 94 +++++ .../flow_api_basic_construct/actions/skip.go | 47 +++ .../actions/start_2fa_recovery.go | 34 ++ .../actions/submit_login_identifier.go | 43 +++ .../actions/submit_new_password.go | 63 ++++ .../actions/submit_new_password_test.go | 150 ++++++++ .../actions/submit_passcode.go | 100 +++++ .../actions/submit_passcode_test.go | 206 ++++++++++ .../actions/submit_password.go | 35 ++ .../actions/submit_recovery_code.go | 35 ++ .../actions/submit_registration_identifier.go | 130 +++++++ .../submit_registration_identifier_test.go | 354 ++++++++++++++++++ .../actions/submit_totp_code.go | 35 ++ .../actions/switch.go | 34 ++ .../common/actions.go | 25 ++ .../flow_api_basic_construct/common/errors.go | 18 + .../flow_api_basic_construct/common/states.go | 34 ++ .../flows/2fa_creation.go | 17 + .../flow_api_basic_construct/flows/login.go | 31 ++ .../flows/passkey_onboarding.go | 48 +++ .../flows/registration.go | 36 ++ backend/flow_api_basic_construct/handler.go | 77 ++++ .../hooks/before_success.go | 134 +++++++ .../services/email.go | 62 +++ .../services/passcode.go | 92 +++++ .../flow_api_test/static/generic_client.html | 32 +- backend/flowpilot/input.go | 2 +- backend/handler/passcode.go | 6 +- backend/handler/passcode_test.go | 10 +- backend/handler/public_router.go | 11 - backend/mail/render.go | 4 +- .../templates/email_verification_text.tmpl | 5 + ...1012141100_create_username_table.down.fizz | 1 + ...231012141100_create_username_table.up.fizz | 9 + ...1013113800_change_passcode_table.down.fizz | 5 + ...231013113800_change_passcode_table.up.fizz | 7 + backend/persistence/models/passcode.go | 20 +- backend/persistence/models/username.go | 23 ++ backend/persistence/username_persister.go | 92 +++++ .../actions/send_capabilities/flows.yaml | 8 + .../actions/submit_new_password/flows.yaml | 14 + .../actions/submit_passcode/flows.yaml | 42 +++ .../actions/submit_passcode/passcodes.yaml | 21 ++ .../emails.yaml | 12 + .../submit_registration_identifier/flows.yaml | 7 + .../usernames.yaml | 10 + .../submit_registration_identifier/users.yaml | 9 + backend/test/persister.go | 8 + 59 files changed, 2805 insertions(+), 34 deletions(-) create mode 100644 backend/flow_api_basic_construct/actions/continue.go create mode 100644 backend/flow_api_basic_construct/actions/generate_recovery_codes.go create mode 100644 backend/flow_api_basic_construct/actions/get_wa_creation_options.go create mode 100644 backend/flow_api_basic_construct/actions/get_wa_request_options.go create mode 100644 backend/flow_api_basic_construct/actions/login_with_oauth.go create mode 100644 backend/flow_api_basic_construct/actions/send_capabilities.go create mode 100644 backend/flow_api_basic_construct/actions/send_capabilities_test.go create mode 100644 backend/flow_api_basic_construct/actions/send_wa_assertion_response.go create mode 100644 backend/flow_api_basic_construct/actions/send_wa_attestation_response.go create mode 100644 backend/flow_api_basic_construct/actions/skip.go create mode 100644 backend/flow_api_basic_construct/actions/start_2fa_recovery.go create mode 100644 backend/flow_api_basic_construct/actions/submit_login_identifier.go create mode 100644 backend/flow_api_basic_construct/actions/submit_new_password.go create mode 100644 backend/flow_api_basic_construct/actions/submit_new_password_test.go create mode 100644 backend/flow_api_basic_construct/actions/submit_passcode.go create mode 100644 backend/flow_api_basic_construct/actions/submit_passcode_test.go create mode 100644 backend/flow_api_basic_construct/actions/submit_password.go create mode 100644 backend/flow_api_basic_construct/actions/submit_recovery_code.go create mode 100644 backend/flow_api_basic_construct/actions/submit_registration_identifier.go create mode 100644 backend/flow_api_basic_construct/actions/submit_registration_identifier_test.go create mode 100644 backend/flow_api_basic_construct/actions/submit_totp_code.go create mode 100644 backend/flow_api_basic_construct/actions/switch.go create mode 100644 backend/flow_api_basic_construct/common/actions.go create mode 100644 backend/flow_api_basic_construct/common/errors.go create mode 100644 backend/flow_api_basic_construct/common/states.go create mode 100644 backend/flow_api_basic_construct/flows/2fa_creation.go create mode 100644 backend/flow_api_basic_construct/flows/login.go create mode 100644 backend/flow_api_basic_construct/flows/passkey_onboarding.go create mode 100644 backend/flow_api_basic_construct/flows/registration.go create mode 100644 backend/flow_api_basic_construct/handler.go create mode 100644 backend/flow_api_basic_construct/hooks/before_success.go create mode 100644 backend/flow_api_basic_construct/services/email.go create mode 100644 backend/flow_api_basic_construct/services/passcode.go create mode 100644 backend/mail/templates/email_verification_text.tmpl create mode 100644 backend/persistence/migrations/20231012141100_create_username_table.down.fizz create mode 100644 backend/persistence/migrations/20231012141100_create_username_table.up.fizz create mode 100644 backend/persistence/migrations/20231013113800_change_passcode_table.down.fizz create mode 100644 backend/persistence/migrations/20231013113800_change_passcode_table.up.fizz create mode 100644 backend/persistence/models/username.go create mode 100644 backend/persistence/username_persister.go create mode 100644 backend/test/fixtures/actions/send_capabilities/flows.yaml create mode 100644 backend/test/fixtures/actions/submit_new_password/flows.yaml create mode 100644 backend/test/fixtures/actions/submit_passcode/flows.yaml create mode 100644 backend/test/fixtures/actions/submit_passcode/passcodes.yaml create mode 100644 backend/test/fixtures/actions/submit_registration_identifier/emails.yaml create mode 100644 backend/test/fixtures/actions/submit_registration_identifier/flows.yaml create mode 100644 backend/test/fixtures/actions/submit_registration_identifier/usernames.yaml create mode 100644 backend/test/fixtures/actions/submit_registration_identifier/users.yaml diff --git a/backend/Dockerfile b/backend/Dockerfile index e7844ffa1..2427e284f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -30,6 +30,7 @@ COPY template template/ COPY utils utils/ COPY mapper mapper/ COPY webhooks webhooks/ +COPY flowpilot flowpilot/ # Build RUN go generate ./... diff --git a/backend/config/config.go b/backend/config/config.go index 8fe1fd177..fcec0d219 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -721,3 +721,45 @@ type Account struct { AllowDeletion bool `yaml:"allow_deletion" json:"allow_deletion,omitempty" koanf:"allow_deletion" jsonschema:"default=false"` AllowSignup bool `yaml:"allow_signup" json:"allow_signup,omitempty" koanf:"allow_signup" jsonschema:"default=true"` } + +// TODO: below structs need validation, e.g. only allowed names for enabled and also we should reject some configurations (e.g. passcode & passwords are disabled and passkey onboarding is also disabled) + +type Identifier struct { + Username IdentifierUsername `yaml:"username" json:"username" koanf:"username"` + Email IdentifierEmail `yaml:"email" json:"email" koanf:"email"` +} + +type IdentifierUsername struct { + Enabled string `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=optional,enum=disabled,enum=optional,enum=required"` + MaxLength int `yaml:"max_length" json:"max_length" koanf:"max_length" split_words:"true"` + MinLength int `yaml:"min_length" json:"min_length" koanf:"min_length" split_words:"true"` + AllowedCharacters string `yaml:"allowed_characters" json:"allowed_characters" koanf:"allowed_characters" split_words:"true"` +} + +type IdentifierEmail struct { + Enabled string `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=optional,enum=disabled,enum=optional,enum=required"` + Verification bool `yaml:"verification" json:"verification" koanf:"verification"` +} + +type SecondFactor struct { + Enabled string `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=optional,enum=disabled,enum=optional,enum=required"` + Onboarding SecondFactorOnboarding `yaml:"onboarding" json:"onboarding" koanf:"onboarding"` + Methods []string `yaml:"methods" json:"methods" koanf:"methods"` // TODO: jsonschema only totp and security_key are allowed + RecoveryCodes RecoveryCodes `yaml:"recovery_codes" json:"recovery_codes" koanf:"recovery_codes" split_words:"true"` +} + +type SecondFactorOnboarding struct { + Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"` +} + +type RecoveryCodes struct { + Enabled string `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=optional,enum=disabled,enum=optional"` +} + +type Passkey struct { + Onboarding PasskeyOnboarding `yaml:"onboarding" json:"onboarding" koanf:"onboarding"` +} + +type PasskeyOnboarding struct { + Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"` +} diff --git a/backend/config/config.yaml b/backend/config/config.yaml index 9098b7d93..c39f79909 100644 --- a/backend/config/config.yaml +++ b/backend/config/config.yaml @@ -16,3 +16,24 @@ secrets: - abcedfghijklmnopqrstuvwxyz service: name: Hanko Authentication Service +webauthn: + relying_party: + id: localhost + origins: + - http://localhost:8000 +password: + enabled: false +emails: + require_verification: true +passkey: + onboarding: + enabled: true +identifier: + username: + enabled: "optional" + max_length: 255 + min_length: 3 + allowed_characters: "abcdefghijklmnopqrstuvwxyz123456789-_." + email: + enabled: "required" + verification: true # maybe we should also consider "disabled|optional|required" or is false interpreted as optional and true as required (and disabled does not exist) diff --git a/backend/flow_api_basic_construct/actions/continue.go b/backend/flow_api_basic_construct/actions/continue.go new file mode 100644 index 000000000..8ca36df9c --- /dev/null +++ b/backend/flow_api_basic_construct/actions/continue.go @@ -0,0 +1,34 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewContinue() Continue { + return Continue{} +} + +type Continue struct{} + +func (m Continue) GetName() flowpilot.ActionName { + return common.ActionContinue +} + +func (m Continue) GetDescription() string { + return "Continue flow." +} + +func (m Continue) Initialize(c flowpilot.InitializationContext) { + // TODO: +} + +func (m Continue) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/generate_recovery_codes.go b/backend/flow_api_basic_construct/actions/generate_recovery_codes.go new file mode 100644 index 000000000..b619f5201 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/generate_recovery_codes.go @@ -0,0 +1,35 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewGenerateRecoveryCodes() GenerateRecoveryCodes { + return GenerateRecoveryCodes{} +} + +type GenerateRecoveryCodes struct{} + +func (m GenerateRecoveryCodes) GetName() flowpilot.ActionName { + return common.ActionGenerateRecoveryCodes +} + +func (m GenerateRecoveryCodes) GetDescription() string { + return "Generate recovery codes." +} + +func (m GenerateRecoveryCodes) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("totp_code").Required(true)) + // TODO: +} + +func (m GenerateRecoveryCodes) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/get_wa_creation_options.go b/backend/flow_api_basic_construct/actions/get_wa_creation_options.go new file mode 100644 index 000000000..8b88c671a --- /dev/null +++ b/backend/flow_api_basic_construct/actions/get_wa_creation_options.go @@ -0,0 +1,129 @@ +package actions + +import ( + "github.com/go-webauthn/webauthn/protocol" + webauthnLib "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/dto/intern" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" +) + +func NewGetWACreationOptions(cfg config.Config, persister persistence.Persister, wa *webauthnLib.WebAuthn) GetWACreationOptions { + return GetWACreationOptions{ + cfg, + persister, + wa, + } +} + +type GetWACreationOptions struct { + cfg config.Config + persister persistence.Persister + wa *webauthnLib.WebAuthn +} + +func (m GetWACreationOptions) GetName() flowpilot.ActionName { + return common.ActionGetWACreationOptions +} + +func (m GetWACreationOptions) GetDescription() string { + return "Get creation options to create a webauthn credential." +} + +func (m GetWACreationOptions) Initialize(c flowpilot.InitializationContext) { + return +} + +func (m GetWACreationOptions) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + userId, err := uuid.NewV4() + if err != nil { + return err + } + if !c.Stash().Get("user_id").Exists() { + err = c.Stash().Set("user_id", userId) + if err != nil { + return err + } + } else { + userId, err = uuid.FromString(c.Stash().Get("user_id").String()) + if err != nil { + return err + } + } + user := WebAuthnUser{ + ID: userId, + Email: c.Stash().Get("email").String(), + Username: c.Stash().Get("username").String(), + } + t := true + options, sessionData, err := m.wa.BeginRegistration( + user, + webauthnLib.WithConveyancePreference(protocol.PreferNoAttestation), + webauthnLib.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ + RequireResidentKey: &t, + ResidentKey: protocol.ResidentKeyRequirementRequired, + UserVerification: protocol.VerificationRequired, + }), + ) + + sessionDataModel := intern.WebauthnSessionDataToModel(sessionData, models.WebauthnOperationRegistration) + err = m.persister.GetWebauthnSessionDataPersister().Create(*sessionDataModel) + if err != nil { + return err + } + + err = c.Stash().Set("webauthn_session_data_id", sessionDataModel.ID) + if err != nil { + return err + } + + err = c.Payload().Set("creationOptions", options) + if err != nil { + return err + } + + return c.ContinueFlow(common.StateOnboardingVerifyPasskeyAttestation) +} + +type WebAuthnUser struct { + ID uuid.UUID + Email string + Username string +} + +func (u WebAuthnUser) WebAuthnID() []byte { + return u.ID.Bytes() +} + +func (u WebAuthnUser) WebAuthnName() string { + if u.Email != "" { + return u.Email + } + + return u.Username +} + +func (u WebAuthnUser) WebAuthnDisplayName() string { + if u.Username != "" { + return u.Username + } + + return u.Email +} + +func (u WebAuthnUser) WebAuthnCredentials() []webauthnLib.Credential { + // TODO: when we use this action also in the profile or in the login flow, then we should/must add the users credentials here. + return nil +} + +func (u WebAuthnUser) WebAuthnIcon() string { + return "" +} diff --git a/backend/flow_api_basic_construct/actions/get_wa_request_options.go b/backend/flow_api_basic_construct/actions/get_wa_request_options.go new file mode 100644 index 000000000..5436559ea --- /dev/null +++ b/backend/flow_api_basic_construct/actions/get_wa_request_options.go @@ -0,0 +1,34 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewGetWARequestOptions() GetWARequestOptions { + return GetWARequestOptions{} +} + +type GetWARequestOptions struct{} + +func (m GetWARequestOptions) GetName() flowpilot.ActionName { + return common.ActionGetWARequestOptions +} + +func (m GetWARequestOptions) GetDescription() string { + return "Get request options to use a webauthn credential." +} + +func (m GetWARequestOptions) Initialize(c flowpilot.InitializationContext) { + // TODO: +} + +func (m GetWARequestOptions) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/login_with_oauth.go b/backend/flow_api_basic_construct/actions/login_with_oauth.go new file mode 100644 index 000000000..89600efb1 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/login_with_oauth.go @@ -0,0 +1,38 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewLoginWithOauth() LoginWithOauth { + return LoginWithOauth{} +} + +type LoginWithOauth struct{} + +func (m LoginWithOauth) GetName() flowpilot.ActionName { + return common.ActionLoginWithOauth +} + +func (m LoginWithOauth) GetDescription() string { + return "Login with a oauth provider." +} + +func (m LoginWithOauth) Initialize(c flowpilot.InitializationContext) { + c.AddInputs( + flowpilot.StringInput("provider").Required(true), + flowpilot.StringInput("redirect_url"), + ) + // TODO: +} + +func (m LoginWithOauth) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/send_capabilities.go b/backend/flow_api_basic_construct/actions/send_capabilities.go new file mode 100644 index 000000000..09d12e579 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/send_capabilities.go @@ -0,0 +1,66 @@ +package actions + +import ( + "errors" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSendCapabilities(cfg config.Config) SendCapabilities { + return SendCapabilities{ + cfg, + } +} + +type SendCapabilities struct { + cfg config.Config +} + +func (m SendCapabilities) GetName() flowpilot.ActionName { + return common.ActionSendCapabilities +} + +func (m SendCapabilities) GetDescription() string { + return "Send the computers capabilities." +} + +func (m SendCapabilities) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("webauthn_available").Required(true).Hidden(true)) +} + +func (m SendCapabilities) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + webauthnAvailable := c.Input().Get("webauthn_available").String() == "true" + + if webauthnAvailable == false && + m.cfg.Password.Enabled == false && + m.cfg.Passcode.Enabled == false { + // Only passkeys are allowed, but webauthn is not available on the browser + return c.ContinueFlowWithError(common.StateError, common.ErrorDeviceNotCapable) + } + if webauthnAvailable == false && + m.cfg.SecondFactor.Enabled == "required" && + len(m.cfg.SecondFactor.Methods) == 1 && + m.cfg.SecondFactor.Methods[0] == "security_key" { + // Only security keys are allowed as a second factor, but webauthn is not available on the browser + return c.ContinueFlowWithError(common.StateError, common.ErrorDeviceNotCapable) + } + + err := c.Stash().Set("webauthn_available", webauthnAvailable) + if err != nil { + return err + } + + switch c.GetCurrentState() { + case common.StateRegistrationPreflight: + return c.ContinueFlow(common.StateRegistrationInit) + case common.StateLoginPreflight: + return c.ContinueFlow(common.StateLoginInit) + default: + return errors.New("unknown parent state") + } +} diff --git a/backend/flow_api_basic_construct/actions/send_capabilities_test.go b/backend/flow_api_basic_construct/actions/send_capabilities_test.go new file mode 100644 index 000000000..970e2e9d7 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/send_capabilities_test.go @@ -0,0 +1,112 @@ +package actions + +import ( + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "testing" +) + +func TestSendCapabilitiesAction(t *testing.T) { + s := new(sendCapabilitiesActionSuite) + + suite.Run(t, s) +} + +type sendCapabilitiesActionSuite struct { + test.Suite +} + +func (s *sendCapabilitiesActionSuite) TestSendCapabilities_Execute() { + if testing.Short() { + s.T().Skip("skipping test in short mode") + } + + tests := []struct { + name string + input string + cfg config.Config + expectedState flowpilot.StateName + statusCode int + }{ + { + name: "webauthn available, passcode disabled, password disabled", + input: `{"webauthn_available": "true"}`, + cfg: config.Config{ + Password: config.Password{Enabled: false}, + Passcode: config.Passcode{Enabled: false}, + SecondFactor: config.SecondFactor{Enabled: "disabled"}, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusOK, + }, + { + name: "webauthn not available, passcode disabled, password disabled", + input: `{"webauthn_available": "false"}`, + cfg: config.Config{ + Password: config.Password{Enabled: false}, + Passcode: config.Passcode{Enabled: false}, + SecondFactor: config.SecondFactor{Enabled: "disabled"}, + }, + expectedState: common.StateError, + statusCode: http.StatusOK, + }, + { + name: "webauthn not available, 2FA required & only security_key is allowed", + input: `{"webauthn_available": "false"}`, + cfg: config.Config{ + Password: config.Password{Enabled: false}, + Passcode: config.Passcode{Enabled: false}, + SecondFactor: config.SecondFactor{Enabled: "disabled"}, + }, + expectedState: common.StateError, + statusCode: http.StatusOK, + }, + { + name: "no input data", + input: "", + cfg: config.Config{ + Password: config.Password{Enabled: false}, + Passcode: config.Passcode{Enabled: false}, + SecondFactor: config.SecondFactor{Enabled: "disabled"}, + }, + expectedState: common.StateRegistrationPreflight, + statusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + s.SetupTest() + defer s.TearDownTest() + + err := s.LoadFixtures("../../test/fixtures/actions/send_capabilities") + s.Require().NoError(err) + + flow, err := flowpilot.NewFlow("/registration_test"). + State(common.StateRegistrationPreflight, NewSendCapabilities(currentTest.cfg)). + State(common.StateRegistrationInit). + State(common.StateError). + State(common.StateSuccess). + InitialState(common.StateRegistrationPreflight). + ErrorState(common.StateError). + Build() + s.Require().NoError(err) + + tx := s.Storage.GetConnection() + db := models.NewFlowDB(tx) + actionParam := "send_capabilities@0b41f4dd-8e46-4a7c-bb4d-d60843113431" + inputData := flowpilot.InputData{JSONString: currentTest.input} + result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData)) + + if s.NoError(err) { + s.Equal(currentTest.statusCode, result.Status()) + s.Equal(currentTest.expectedState, result.Response().StateName) + } + }) + } +} diff --git a/backend/flow_api_basic_construct/actions/send_wa_assertion_response.go b/backend/flow_api_basic_construct/actions/send_wa_assertion_response.go new file mode 100644 index 000000000..e051adb8d --- /dev/null +++ b/backend/flow_api_basic_construct/actions/send_wa_assertion_response.go @@ -0,0 +1,35 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSendWAAssertionResponse() SendWAAssertionResponse { + return SendWAAssertionResponse{} +} + +type SendWAAssertionResponse struct{} + +func (m SendWAAssertionResponse) GetName() flowpilot.ActionName { + return common.ActionSendWAAssertionResponse +} + +func (m SendWAAssertionResponse) GetDescription() string { + return "Send the result which was generated by using a webauthn credential." +} + +func (m SendWAAssertionResponse) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("public_key").Required(true)) + // TODO: +} + +func (m SendWAAssertionResponse) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/send_wa_attestation_response.go b/backend/flow_api_basic_construct/actions/send_wa_attestation_response.go new file mode 100644 index 000000000..a1c7055ee --- /dev/null +++ b/backend/flow_api_basic_construct/actions/send_wa_attestation_response.go @@ -0,0 +1,94 @@ +package actions + +import ( + "github.com/go-webauthn/webauthn/protocol" + webauthnLib "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/dto/intern" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/session" + "strings" +) + +func NewSendWAAttestationResponse(cfg config.Config, persister persistence.Persister, wa *webauthnLib.WebAuthn, sessionManager session.Manager, httpContext echo.Context) SendWAAttestationResponse { + return SendWAAttestationResponse{ + cfg, + persister, + wa, + sessionManager, + httpContext, + } +} + +type SendWAAttestationResponse struct { + cfg config.Config + persister persistence.Persister + wa *webauthnLib.WebAuthn + sessionManager session.Manager + httpContext echo.Context +} + +func (m SendWAAttestationResponse) GetName() flowpilot.ActionName { + return common.ActionSendWAAttestationResponse +} + +func (m SendWAAttestationResponse) GetDescription() string { + return "Send the result which was generated by creating a webauthn credential." +} + +func (m SendWAAttestationResponse) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("public_key").Required(true).Persist(false)) +} + +func (m SendWAAttestationResponse) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + response, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(c.Input().Get("public_key").String())) + if err != nil { + return err + } + + sessionDataId, err := uuid.FromString(c.Stash().Get("webauthn_session_data_id").String()) + if err != nil { + return err + } + sessionData, err := m.persister.GetWebauthnSessionDataPersister().Get(sessionDataId) + if err != nil { + return err + } + userId, err := uuid.FromString(c.Stash().Get("user_id").String()) + if err != nil { + return err + } + webauthnUser := WebAuthnUser{ + ID: userId, + Email: c.Stash().Get("email").String(), + Username: c.Stash().Get("username").String(), + } + + credential, err := m.wa.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), response) + if err != nil { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid.Wrap(err)) + } + + err = c.Stash().Set("passkey_credential", credential) + if err != nil { + return err + } + + err = m.persister.GetWebauthnSessionDataPersister().Delete(*sessionData) + if err != nil { + // TODO: should we return an error here or just log the error because it is not that critical to delete + // the session data as the flow will be marked as complete directly afterwards and the session data are + // linked to the flow + return err + } + + return c.EndSubFlow() +} diff --git a/backend/flow_api_basic_construct/actions/skip.go b/backend/flow_api_basic_construct/actions/skip.go new file mode 100644 index 000000000..41d49c584 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/skip.go @@ -0,0 +1,47 @@ +package actions + +import ( + "errors" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSkip(cfg config.Config) Skip { + return Skip{ + cfg, + } +} + +type Skip struct { + cfg config.Config +} + +func (m Skip) GetName() flowpilot.ActionName { + return common.ActionSkip +} + +func (m Skip) GetDescription() string { + return "Skip the current state." +} + +func (m Skip) Initialize(c flowpilot.InitializationContext) { + if !m.cfg.Passcode.Enabled && !m.cfg.Password.Enabled { + // suspend action when only passkeys are allowed + c.SuspendAction() + } +} + +func (m Skip) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + switch c.GetCurrentState() { + case common.StateOnboardingCreatePasskey: + return c.EndSubFlow() + default: + // return an error, so we don't implicitly continue to unwanted state + return errors.New("no destination is defined") + } +} diff --git a/backend/flow_api_basic_construct/actions/start_2fa_recovery.go b/backend/flow_api_basic_construct/actions/start_2fa_recovery.go new file mode 100644 index 000000000..7100d7901 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/start_2fa_recovery.go @@ -0,0 +1,34 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewStart2FARecovery() Start2FARecovery { + return Start2FARecovery{} +} + +type Start2FARecovery struct{} + +func (m Start2FARecovery) GetName() flowpilot.ActionName { + return common.ActionStart2FARecovery +} + +func (m Start2FARecovery) GetDescription() string { + return "Start a second factor recovery." +} + +func (m Start2FARecovery) Initialize(c flowpilot.InitializationContext) { + // TODO: +} + +func (m Start2FARecovery) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_login_identifier.go b/backend/flow_api_basic_construct/actions/submit_login_identifier.go new file mode 100644 index 000000000..2ef57d8ab --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_login_identifier.go @@ -0,0 +1,43 @@ +package actions + +import ( + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" +) + +func NewSubmitLoginIdentifier(persister persistence.Persister, httpContext echo.Context) SubmitLoginIdentifier { + return SubmitLoginIdentifier{ + persister, + httpContext, + } +} + +type SubmitLoginIdentifier struct { + persister persistence.Persister + httpContext echo.Context +} + +func (m SubmitLoginIdentifier) GetName() flowpilot.ActionName { + return common.ActionSubmitLoginIdentifier +} + +func (m SubmitLoginIdentifier) GetDescription() string { + return "Enter an identifier to login." +} + +func (m SubmitLoginIdentifier) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.EmailInput("identifier").Required(true).Preserve(true)) + // TODO: +} + +func (m SubmitLoginIdentifier) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_new_password.go b/backend/flow_api_basic_construct/actions/submit_new_password.go new file mode 100644 index 000000000..bf014b543 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_new_password.go @@ -0,0 +1,63 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "golang.org/x/crypto/bcrypt" + "unicode/utf8" +) + +func NewSubmitNewPassword(cfg config.Config) SubmitNewPassword { + return SubmitNewPassword{ + cfg, + } +} + +type SubmitNewPassword struct { + cfg config.Config +} + +func (m SubmitNewPassword) GetName() flowpilot.ActionName { + return common.ActionSubmitNewPassword +} + +func (m SubmitNewPassword) GetDescription() string { + return "Submit a new password." +} + +func (m SubmitNewPassword) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.PasswordInput("new_password").Required(true).MinLength(m.cfg.Password.MinPasswordLength)) +} + +func (m SubmitNewPassword) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + newPassword := c.Input().Get("new_password").String() + newPasswordBytes := []byte(newPassword) + if utf8.RuneCountInString(newPassword) < m.cfg.Password.MinPasswordLength { + c.Input().SetError("new_password", flowpilot.ErrorValueInvalid) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + if len(newPasswordBytes) > 72 { + c.Input().SetError("new_password", flowpilot.ErrorValueInvalid) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(c.Input().Get("new_password").String()), 12) + if err != nil { + return err + } + err = c.Stash().Set("new_password", string(hashedPassword)) + + // Decide which is the next state according to the config and user input + if m.cfg.Passkey.Onboarding.Enabled && c.Stash().Get("webauthn_available").Bool() { + return c.StartSubFlow(common.StateOnboardingCreatePasskey, common.StateSuccess) + } + // TODO: 2FA routing + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_new_password_test.go b/backend/flow_api_basic_construct/actions/submit_new_password_test.go new file mode 100644 index 000000000..087df376e --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_new_password_test.go @@ -0,0 +1,150 @@ +package actions + +import ( + "fmt" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/test" + "log" + "net/http" + "testing" +) + +func TestSubmitNewPassword(t *testing.T) { + s := new(submitNewPassword) + + suite.Run(t, s) +} + +type submitNewPassword struct { + test.Suite +} + +func (s *submitNewPassword) TestSubmitNewPassword_Execute() { + if testing.Short() { + s.T().Skip("skipping test in short mode") + } + + tests := []struct { + name string + input string + flowId string + cfg config.Config + expectedState flowpilot.StateName + statusCode int + }{ + { + name: "submit a new password", + input: `{"new_password": "SuperSecure"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{}, + expectedState: common.StateSuccess, + statusCode: http.StatusOK, + }, + { + name: "submit a new password that is too short", + input: `{"new_password": "test"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Password: config.Password{ + MinPasswordLength: 8, + }, + }, + expectedState: common.StatePasswordCreation, + statusCode: http.StatusBadRequest, + }, + { + name: "submit a password that is too long", + input: `{"new_password": "ThisIsAVeryVeryLongPasswordToCheckTheLengthCheckAndItMustBeVeryLongInOrderToDoSo"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{}, + expectedState: common.StatePasswordCreation, + statusCode: http.StatusBadRequest, + }, + { + name: "submit a new password and webauthn is not available and passkey onboarding is enabled", + input: `{"new_password": "SuperSecure"}`, + flowId: "8a2cf90d-dea5-4678-9dca-6707dab6af77", + cfg: config.Config{ + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: true, + }, + }, + }, + expectedState: common.StateSuccess, + statusCode: http.StatusOK, + }, + { + name: "submit a new password and webauthn is available and passkey onboarding is disabled", + input: `{"new_password": "SuperSecure"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: false, + }, + }, + }, + expectedState: common.StateSuccess, + statusCode: http.StatusOK, + }, + { + name: "submit a new password and webauthn is available and passkey onboarding is enabled", + input: `{"new_password": "SuperSecure"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: true, + }, + }, + }, + expectedState: common.StateOnboardingCreatePasskey, + statusCode: http.StatusOK, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + s.SetupTest() + defer s.TearDownTest() + + err := s.LoadFixtures("../../test/fixtures/actions/submit_new_password") + s.Require().NoError(err) + + passkeySubFlow, err := flowpilot.NewSubFlow(). + State(common.StateOnboardingCreatePasskey). + Build() + s.Require().NoError(err) + + flow, err := flowpilot.NewFlow("/registration_test"). + State(common.StatePasswordCreation, NewSubmitNewPassword(currentTest.cfg)). + State(common.StateSuccess). + SubFlows(passkeySubFlow). + InitialState(common.StatePasswordCreation). + ErrorState(common.StateError). + Debug(true). + Build() + s.Require().NoError(err) + + tx := s.Storage.GetConnection() + db := models.NewFlowDB(tx) + actionParam := fmt.Sprintf("submit_new_password@%s", currentTest.flowId) + inputData := flowpilot.InputData{JSONString: currentTest.input} + result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData)) + s.Require().NoError(err) + + s.Equal(currentTest.statusCode, result.Status()) + s.Equal(currentTest.expectedState, result.Response().StateName) + + log.Println(result.Response().PublicError) + // TODO: check that the schema of the action returns the correct error_code e.g. + // result.Response().PublicActions[0].PublicSchema[0].PublicError.Code == ErrorValueInvalid + }) + } + +} diff --git a/backend/flow_api_basic_construct/actions/submit_passcode.go b/backend/flow_api_basic_construct/actions/submit_passcode.go new file mode 100644 index 000000000..cfc94c67a --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_passcode.go @@ -0,0 +1,100 @@ +package actions + +import ( + "errors" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "golang.org/x/crypto/bcrypt" + "time" +) + +var maxPasscodeTries = 3 + +func NewSubmitPasscode(cfg config.Config, persister persistence.Persister) SubmitPasscode { + return SubmitPasscode{ + cfg, + persister, + } +} + +type SubmitPasscode struct { + cfg config.Config + persister persistence.Persister +} + +func (m SubmitPasscode) GetName() flowpilot.ActionName { + return common.ActionSubmitPasscode +} + +func (m SubmitPasscode) GetDescription() string { + return "Enter a passcode." +} + +func (m SubmitPasscode) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("code").Required(true)) +} + +func (m SubmitPasscode) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + passcodeId, err := uuid.FromString(c.Stash().Get("passcode_id").String()) + if err != nil { + return err + } + + passcode, err := m.persister.GetPasscodePersister().Get(passcodeId) + if err != nil { + return err + } + if passcode == nil { + return errors.New("passcode not found") + } + + expirationTime := passcode.CreatedAt.Add(time.Duration(passcode.Ttl) * time.Second) + if expirationTime.Before(time.Now().UTC()) { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid.Wrap(errors.New("passcode is expired"))) + } + + err = bcrypt.CompareHashAndPassword([]byte(passcode.Code), []byte(c.Input().Get("code").String())) + if err != nil { + passcode.TryCount += 1 + if passcode.TryCount >= maxPasscodeTries { + err = m.persister.GetPasscodePersister().Delete(*passcode) + if err != nil { + return err + } + err = c.Stash().Delete("passcode_id") + if err != nil { + return err + } + + return c.ContinueFlowWithError(c.GetCurrentState(), common.ErrorPasscodeMaxAttemptsReached) + } + return c.ContinueFlowWithError(c.GetCurrentState(), common.ErrorPasscodeInvalid.Wrap(err)) + } + + err = c.Stash().Set("email_verified", true) // TODO: maybe change attribute path + if err != nil { + return err + } + + err = m.persister.GetPasscodePersister().Delete(*passcode) + if err != nil { + return err + } + + // TODO: This the current routing is only for the registration flow, when this action is/will be used in the login flow on other states, then the routing needs to be changed accordingly + // Decide which is the next state according to the config and user input + if m.cfg.Password.Enabled { + return c.ContinueFlow(common.StatePasswordCreation) + } else if !m.cfg.Passcode.Enabled || (m.cfg.Passkey.Onboarding.Enabled && c.Stash().Get("webauthn_available").Bool()) { + return c.StartSubFlow(common.StateOnboardingCreatePasskey, common.StateSuccess) + } + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_passcode_test.go b/backend/flow_api_basic_construct/actions/submit_passcode_test.go new file mode 100644 index 000000000..3c6f2f154 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_passcode_test.go @@ -0,0 +1,206 @@ +package actions + +import ( + "fmt" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/test" + "golang.org/x/crypto/bcrypt" + "log" + "net/http" + "testing" +) + +func TestSubmitPasscode(t *testing.T) { + s := new(submitPasscodeActionSuite) + + suite.Run(t, s) +} + +type submitPasscodeActionSuite struct { + test.Suite +} + +func (s *submitPasscodeActionSuite) TestSubmitPasscode_Execute() { + if testing.Short() { + s.T().Skip("skipping test in short mode") + } + + tests := []struct { + name string + input string + cfg config.Config + expectedState flowpilot.StateName + statusCode int + flowId string + expectsFlowError bool + }{ + { + name: "submit a correct passcode", + input: `{"code": "123456"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Passcode: config.Passcode{ + Enabled: true, + }, + }, + expectedState: common.StateSuccess, + statusCode: http.StatusOK, + }, + { + name: "submit a wrong passcode", + input: `{"code": "654321"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{}, + expectedState: common.StateEmailVerification, + statusCode: http.StatusUnauthorized, + }, + { + name: "submit a passcode which max attempts are reached", + input: `{"code": "654321"}`, + flowId: "8a2cf90d-dea5-4678-9dca-6707dab6af77", + cfg: config.Config{}, + expectedState: common.StateEmailVerification, + statusCode: http.StatusUnauthorized, + }, + { + name: "submit a passcode where the id is not in the stash", + input: `{"code": "123456"}`, + flowId: "23524801-f445-4859-bc16-22cf1dd417ac", + cfg: config.Config{}, + expectsFlowError: true, + expectedState: common.StateError, + statusCode: http.StatusInternalServerError, + }, + { + name: "submit a passcode where the passcode is not stored in the DB", + input: `{"code": "123456"}`, + flowId: "fc4dc7e4-bce7-4154-873b-cb3d766df279", + expectsFlowError: true, + cfg: config.Config{}, + expectedState: common.StateError, + statusCode: http.StatusInternalServerError, + }, + { + name: "submit a passcode where the passcode is expired", + input: `{"code": "123456"}`, + flowId: "5a862a2d-0d10-4904-b297-cb32fc43c859", + cfg: config.Config{}, + expectedState: common.StateEmailVerification, + statusCode: http.StatusBadRequest, + }, + { + name: "submit a correct passcode and passwords are enabled", + input: `{"code": "123456"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Password: config.Password{ + Enabled: true, + }, + }, + expectedState: common.StatePasswordCreation, + statusCode: http.StatusOK, + }, + { + name: "submit a correct passcode and passcodes for login are disabled and passkey onboarding is disabled", + input: `{"code": "123456"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Passcode: config.Passcode{ + Enabled: false, + }, + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: false, + }, + }, + }, + expectedState: common.StateOnboardingCreatePasskey, + statusCode: http.StatusOK, + }, + { + name: "submit a correct passcode and passkey onboarding is enabled and webauthn is available", + input: `{"code": "123456"}`, + flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431", + cfg: config.Config{ + Passcode: config.Passcode{ + Enabled: true, + }, + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: true, + }, + }, + }, + expectedState: common.StateOnboardingCreatePasskey, + statusCode: http.StatusOK, + }, + { + name: "submit a correct passcode and passkey onboarding is enabled and webauthn is not available", + input: `{"code": "123456"}`, + flowId: "bc3173e7-3204-4b9a-904b-9f812330b0de", + cfg: config.Config{ + Passcode: config.Passcode{ + Enabled: true, + }, + Passkey: config.Passkey{ + Onboarding: config.PasskeyOnboarding{ + Enabled: true, + }, + }, + }, + expectedState: common.StateSuccess, + statusCode: http.StatusOK, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + s.SetupTest() + defer s.TearDownTest() + + err := s.LoadFixtures("../../test/fixtures/actions/submit_passcode") + s.Require().NoError(err) + + passkeySubFlow, err := flowpilot.NewSubFlow(). + State(common.StateOnboardingCreatePasskey). + Build() + s.Require().NoError(err) + + flow, err := flowpilot.NewFlow("/registration_test"). + State(common.StateEmailVerification, NewSubmitPasscode(currentTest.cfg, s.Storage)). + State(common.StatePasswordCreation). + State(common.StateSuccess). + State(common.StateError). + InitialState(common.StateEmailVerification). + ErrorState(common.StateError). + SubFlows(passkeySubFlow). + Build() + s.Require().NoError(err) + + tx := s.Storage.GetConnection() + db := models.NewFlowDB(tx) + actionParam := fmt.Sprintf("submit_email_passcode@%s", currentTest.flowId) + inputData := flowpilot.InputData{JSONString: currentTest.input} + result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData)) + if currentTest.expectsFlowError { + s.Require().Error(err) + } else { + s.Require().NoError(err) + + s.Equal(currentTest.statusCode, result.Status()) + s.Equal(currentTest.expectedState, result.Response().StateName) + // TODO: check that the schema of the action returns the correct error_code e.g. + // result.Response().PublicActions[0].PublicSchema[0].PublicError.Code == ErrorValueInvalid + } + }) + } +} + +func TestName(t *testing.T) { + hash, _ := bcrypt.GenerateFromPassword([]byte("123456"), 12) + log.Println(string(hash)) +} diff --git a/backend/flow_api_basic_construct/actions/submit_password.go b/backend/flow_api_basic_construct/actions/submit_password.go new file mode 100644 index 000000000..ee6cc7501 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_password.go @@ -0,0 +1,35 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSubmitPassword() SubmitPassword { + return SubmitPassword{} +} + +type SubmitPassword struct{} + +func (m SubmitPassword) GetName() flowpilot.ActionName { + return common.ActionSubmitPassword +} + +func (m SubmitPassword) GetDescription() string { + return "Login with a password." +} + +func (m SubmitPassword) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.PasswordInput("password").Required(true)) + // TODO: +} + +func (m SubmitPassword) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_recovery_code.go b/backend/flow_api_basic_construct/actions/submit_recovery_code.go new file mode 100644 index 000000000..cda6ebbab --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_recovery_code.go @@ -0,0 +1,35 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSubmitRecoveryCode() SubmitRecoveryCode { + return SubmitRecoveryCode{} +} + +type SubmitRecoveryCode struct{} + +func (m SubmitRecoveryCode) GetName() flowpilot.ActionName { + return common.ActionSubmitRecoveryCode +} + +func (m SubmitRecoveryCode) GetDescription() string { + return "Submit a recovery code." +} + +func (m SubmitRecoveryCode) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("recovery_code").Required(true)) + // TODO: +} + +func (m SubmitRecoveryCode) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_registration_identifier.go b/backend/flow_api_basic_construct/actions/submit_registration_identifier.go new file mode 100644 index 000000000..4229f2dae --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_registration_identifier.go @@ -0,0 +1,130 @@ +package actions + +import ( + "errors" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/services" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "strings" +) + +func NewSubmitRegistrationIdentifier(cfg config.Config, persister persistence.Persister, passcodeService services.Passcode, httpContext echo.Context) SubmitRegistrationIdentifier { + return SubmitRegistrationIdentifier{ + cfg, + persister, + httpContext, + passcodeService, + } +} + +// SubmitRegistrationIdentifier takes the identifier which the user entered and checks if they are valid and available according to the configuration +type SubmitRegistrationIdentifier struct { + cfg config.Config + persister persistence.Persister + httpContext echo.Context + passcodeService services.Passcode +} + +func (m SubmitRegistrationIdentifier) GetName() flowpilot.ActionName { + return common.ActionSubmitRegistrationIdentifier +} + +func (m SubmitRegistrationIdentifier) GetDescription() string { + return "Enter an identifier to register." +} + +func (m SubmitRegistrationIdentifier) Initialize(c flowpilot.InitializationContext) { + inputs := make([]flowpilot.Input, 0) + emailInput := flowpilot.EmailInput("email").MaxLength(255).Persist(true).Preserve(true) + if m.cfg.Identifier.Email.Enabled == "optional" { + emailInput.Required(false) + inputs = append(inputs, emailInput) + } else if m.cfg.Identifier.Email.Enabled == "required" { + emailInput.Required(true) + inputs = append(inputs, emailInput) + } + + usernameInput := flowpilot.StringInput("username").MinLength(m.cfg.Identifier.Username.MinLength).MaxLength(m.cfg.Identifier.Username.MaxLength).Persist(true).Preserve(true) + if m.cfg.Identifier.Username.Enabled == "optional" { + usernameInput.Required(false) + inputs = append(inputs, usernameInput) + } else if m.cfg.Identifier.Username.Enabled == "required" { + usernameInput.Required(true) + inputs = append(inputs, usernameInput) + } + c.AddInputs(inputs...) +} + +func (m SubmitRegistrationIdentifier) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + email := c.Input().Get("email").String() + username := c.Input().Get("username").String() + + for _, char := range username { + // check that username only contains allowed characters + if !strings.Contains(m.cfg.Identifier.Username.AllowedCharacters, string(char)) { + c.Input().SetError("username", flowpilot.ErrorValueInvalid.Wrap(errors.New("username contains invalid characters"))) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + } + + if email != "" { + // Check that email is not already taken + // this check is non-exhaustive as the email is not blocked here and might be created after the check here and the user creation + e, err := m.persister.GetEmailPersister().FindByAddress(email) + if err != nil { + return err + } + // Do not return an error when only identifier is email and email verification is on (account enumeration protection) + if e != nil && !(m.cfg.Identifier.Username.Enabled == "disabled" && m.cfg.Emails.RequireVerification) { + c.Input().SetError("email", common.ErrorEmailAlreadyExists) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + } + + if username != "" { + // Check that username is not already taken + // this check is non-exhaustive as the username is not blocked here and might be created after the check here and the user creation + e, err := m.persister.GetUsernamePersister().Find(username) + if err != nil { + return err + } + if e != nil { + c.Input().SetError("username", common.ErrorUsernameAlreadyExists) + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + } + + err := c.CopyInputValuesToStash("email", "username") + if err != nil { + return err + } + + // Decide which is the next state according to the config and user input + if email != "" && m.cfg.Emails.RequireVerification { + // TODO: rate limit sending emails + passcodeId, err := m.passcodeService.SendEmailVerification(c.GetFlowID(), email, m.httpContext.Request().Header.Get("Accept-Language")) + if err != nil { + return err + } + err = c.Stash().Set("passcode_id", passcodeId) + if err != nil { + return err + } + return c.ContinueFlow(common.StateEmailVerification) + } else if m.cfg.Password.Enabled { + return c.ContinueFlow(common.StatePasswordCreation) + } else if !m.cfg.Passcode.Enabled { + return c.StartSubFlow(common.StateOnboardingCreatePasskey, common.StateSuccess) + } + + // TODO: store user and create session token // should this case even exist (only works when email (optional/required) is set by the user) ??? + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/submit_registration_identifier_test.go b/backend/flow_api_basic_construct/actions/submit_registration_identifier_test.go new file mode 100644 index 000000000..9d414e18b --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_registration_identifier_test.go @@ -0,0 +1,354 @@ +package actions + +import ( + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSubmitRegistrationIdentifier(t *testing.T) { + s := new(submitRegistrationIdentifierActionSuite) + + suite.Run(t, s) +} + +type submitRegistrationIdentifierActionSuite struct { + test.Suite +} + +func (s *submitRegistrationIdentifierActionSuite) TestSubmitRegistrationIdentifier_Execute() { + if testing.Short() { + s.T().Skip("skipping test in short mode") + } + + tests := []struct { + name string + input string + cfg config.Config + expectedState flowpilot.StateName + statusCode int + }{ + { + name: "can register a new user with email", + input: `{"email":"new@example.com"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "disabled", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: true, + }, + }, + expectedState: common.StateEmailVerification, + statusCode: http.StatusOK, + }, + { + name: "can register a new user with username", + input: `{"username":"new_user"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "disabled", + }, + }, + }, + expectedState: common.StateOnboardingCreatePasskey, + statusCode: http.StatusOK, + }, + { + name: "can register a new user with email and username", + input: `{"email":"new@exmaple.com","username":"new_user"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: true, + }, + }, + expectedState: common.StateEmailVerification, + statusCode: http.StatusOK, + }, + { + name: "cannot register a new user with existing email", + input: `{"email":"john.doe@example.com", "username": ""}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "optional", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: true, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "do not return an error when user enumeration protection is implicit enabled", + input: `{"email":"john.doe@example.com", "username": ""}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "disabled", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: true, + }, + }, + expectedState: common.StateEmailVerification, + statusCode: http.StatusOK, + }, + { + name: "cannot register a new user with existing username", + input: `{"username":"john.doe"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "disabled", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "cannot register a new user missing required email", + input: `{"username":"new_user"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "optional", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "cannot register a new user missing required username", + input: `{"email":"new@example.com"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "optional", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "cannot register a new user with username with not allowed characters", + input: `{"username": "unwanted ch@r@cters"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "disabled", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "cannot register a new user with too short username", + input: `{"username": "t"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "disabled", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "cannot register a new user with too long username", + input: `{"username":"this_is_a_very_very_long_username_to_check_if_this_username_is_rejected"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "required", + MaxLength: 64, + MinLength: 3, + AllowedCharacters: "abcdefghijklmnopqrstuvwxyz123456789-_.", + }, + Email: config.IdentifierEmail{ + Enabled: "disabled", + }, + }, + }, + expectedState: common.StateRegistrationInit, + statusCode: http.StatusBadRequest, + }, + { + name: "can register a new user with email verification disabled and password disabled", + input: `{"email": "new@example.com"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "disabled", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: false, + }, + Password: config.Password{ + Enabled: false, + }, + }, + expectedState: common.StateOnboardingCreatePasskey, + statusCode: http.StatusOK, + }, + { + name: "can register a new user with password enabled and email verification disabled", + input: `{"email": "new@example.com"}`, + cfg: config.Config{ + Identifier: config.Identifier{ + Username: config.IdentifierUsername{ + Enabled: "disabled", + }, + Email: config.IdentifierEmail{ + Enabled: "required", + }, + }, + Emails: config.Emails{ + RequireVerification: false, + }, + Password: config.Password{ + Enabled: true, + }, + }, + expectedState: common.StatePasswordCreation, + statusCode: http.StatusOK, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + s.SetupTest() + defer s.TearDownTest() + + err := s.LoadFixtures("../../test/fixtures/actions/submit_registration_identifier") + s.Require().NoError(err) + + req := httptest.NewRequest(http.MethodPost, "/passcode/login/initialize", nil) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + passkeySubFlow, err := flowpilot.NewSubFlow(). + State(common.StateOnboardingCreatePasskey). + Build() + s.Require().NoError(err) + + flow, err := flowpilot.NewFlow("/registration_test"). + State(common.StateRegistrationInit, NewSubmitRegistrationIdentifier(currentTest.cfg, s.Storage, &testPasscodeService{}, echo.New().NewContext(req, rec))). + State(common.StateEmailVerification). + State(common.StateSuccess). + State(common.StatePasswordCreation). + SubFlows(passkeySubFlow). + InitialState(common.StateRegistrationInit). + ErrorState(common.StateError). + Build() + s.Require().NoError(err) + + tx := s.Storage.GetConnection() + db := models.NewFlowDB(tx) + actionParam := "submit_registration_identifier@0b41f4dd-8e46-4a7c-bb4d-d60843113431" + inputData := flowpilot.InputData{JSONString: currentTest.input} + result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData)) + s.Require().NoError(err) + + s.Equal(currentTest.statusCode, result.Status()) + s.Equal(currentTest.expectedState, result.Response().StateName) + // TODO: check that the schema of the action returns the correct error_code e.g. + // result.Response().PublicActions[0].PublicSchema[0].PublicError.Code == ErrorValueInvalid + }) + } +} + +// TODO: should be removed, tests should use new email test server instance introduced in https://github.com/teamhanko/hanko/pull/1093 +type testPasscodeService struct { +} + +func (t *testPasscodeService) SendEmailVerification(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return uuid.NewV4() +} + +func (t *testPasscodeService) SendLogin(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return uuid.NewV4() +} + +func (t *testPasscodeService) PasswordRecovery(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return uuid.NewV4() +} diff --git a/backend/flow_api_basic_construct/actions/submit_totp_code.go b/backend/flow_api_basic_construct/actions/submit_totp_code.go new file mode 100644 index 000000000..e83601d19 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/submit_totp_code.go @@ -0,0 +1,35 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSubmitTOTPCode() SubmitTOTPCode { + return SubmitTOTPCode{} +} + +type SubmitTOTPCode struct{} + +func (m SubmitTOTPCode) GetName() flowpilot.ActionName { + return common.ActionSubmitTOTPCode +} + +func (m SubmitTOTPCode) GetDescription() string { + return "Submit a TOTP code." +} + +func (m SubmitTOTPCode) Initialize(c flowpilot.InitializationContext) { + c.AddInputs(flowpilot.StringInput("totp_code").Required(true)) + // TODO: +} + +func (m SubmitTOTPCode) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/actions/switch.go b/backend/flow_api_basic_construct/actions/switch.go new file mode 100644 index 000000000..e9a093685 --- /dev/null +++ b/backend/flow_api_basic_construct/actions/switch.go @@ -0,0 +1,34 @@ +package actions + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func NewSwitch() Switch { + return Switch{} +} + +type Switch struct{} + +func (m Switch) GetName() flowpilot.ActionName { + return common.ActionSwitch +} + +func (m Switch) GetDescription() string { + return "Switch to a different state." +} + +func (m Switch) Initialize(c flowpilot.InitializationContext) { + // TODO: +} + +func (m Switch) Execute(c flowpilot.ExecutionContext) error { + if valid := c.ValidateInputData(); !valid { + return c.ContinueFlowWithError(c.GetCurrentState(), flowpilot.ErrorFormDataInvalid) + } + + // TODO: + + return c.ContinueFlow(common.StateSuccess) +} diff --git a/backend/flow_api_basic_construct/common/actions.go b/backend/flow_api_basic_construct/common/actions.go new file mode 100644 index 000000000..864547828 --- /dev/null +++ b/backend/flow_api_basic_construct/common/actions.go @@ -0,0 +1,25 @@ +package common + +import "github.com/teamhanko/hanko/backend/flowpilot" + +const ( + ActionSendCapabilities flowpilot.ActionName = "send_capabilities" + ActionLoginWithOauth flowpilot.ActionName = "login_with_oauth" + ActionSubmitRegistrationIdentifier flowpilot.ActionName = "submit_registration_identifier" + ActionSubmitLoginIdentifier flowpilot.ActionName = "submit_login_identifier" + ActionSubmitPasscode flowpilot.ActionName = "submit_email_passcode" + ActionGetWARequestOptions flowpilot.ActionName = "get_wa_request_options" + ActionSendWAAssertionResponse flowpilot.ActionName = "send_wa_request_response" + ActionGetWACreationOptions flowpilot.ActionName = "get_wa_creation_options" + ActionSendWAAttestationResponse flowpilot.ActionName = "send_wa_attestation_options" + ActionSubmitPassword flowpilot.ActionName = "submit_password" + ActionSubmitNewPassword flowpilot.ActionName = "submit_new_password" + ActionSubmitTOTPCode flowpilot.ActionName = "submit_totp_code" + ActionGenerateRecoveryCodes flowpilot.ActionName = "generate_recovery_codes" + ActionStart2FARecovery flowpilot.ActionName = "start_2fa_recovery" + ActionSubmitRecoveryCode flowpilot.ActionName = "submit_recovery_code" + + ActionSwitch flowpilot.ActionName = "switch" + ActionSkip flowpilot.ActionName = "skip" + ActionContinue flowpilot.ActionName = "continue" +) diff --git a/backend/flow_api_basic_construct/common/errors.go b/backend/flow_api_basic_construct/common/errors.go new file mode 100644 index 000000000..dc99178ef --- /dev/null +++ b/backend/flow_api_basic_construct/common/errors.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/teamhanko/hanko/backend/flowpilot" + "net/http" +) + +var ( + ErrorConfigurationError = flowpilot.NewFlowError("configuration_error", "The configuration contains errors.", http.StatusInternalServerError) + ErrorDeviceNotCapable = flowpilot.NewFlowError("device_not_capable", "The device can not login or register.", http.StatusOK) // The device is not able to provide at least one login method. + ErrorPasscodeInvalid = flowpilot.NewFlowError("passcode_invalid", "The passcode is invalid.", http.StatusUnauthorized) + ErrorPasscodeMaxAttemptsReached = flowpilot.NewFlowError("passcode_max_attempts_reached", "The passcode was entered wrong too many times.", http.StatusUnauthorized) +) + +var ( + ErrorEmailAlreadyExists = flowpilot.NewInputError("email_already_exists", "The email address already exists.") + ErrorUsernameAlreadyExists = flowpilot.NewInputError("username_already_exists", "The username already exists.") +) diff --git a/backend/flow_api_basic_construct/common/states.go b/backend/flow_api_basic_construct/common/states.go new file mode 100644 index 000000000..f6b8373de --- /dev/null +++ b/backend/flow_api_basic_construct/common/states.go @@ -0,0 +1,34 @@ +package common + +import "github.com/teamhanko/hanko/backend/flowpilot" + +const ( + StateSuccess flowpilot.StateName = "success" + StateError flowpilot.StateName = "error" + + StateLoginPreflight flowpilot.StateName = "login_preflight" + StateLoginInit flowpilot.StateName = "login_init" + StateLoginMethodChooser flowpilot.StateName = "login_method_chooser" + StatePasswordLogin flowpilot.StateName = "password_login" + StateLoginPasscodeConfirmation flowpilot.StateName = "login_passcode_confirmation" + StateRecoveryPasscodeConfirmation flowpilot.StateName = "recovery_passcode_confirmation" + StatePasskeyLogin flowpilot.StateName = "passkey_login" + StateUse2FATOTP flowpilot.StateName = "use_2fa_totp" + StateUse2FASecurityKey flowpilot.StateName = "use_2fa_security_key" + StateUseRecoveryCode flowpilot.StateName = "use_recovery_code" + StateRecoveryPasswordCreation flowpilot.StateName = "recovery_password_creation" + + StateRegistrationPreflight flowpilot.StateName = "registration_preflight" + StateRegistrationInit flowpilot.StateName = "registration_init" + StateEmailVerification flowpilot.StateName = "registration_email_verification" + StatePasswordCreation flowpilot.StateName = "password_creation" + + StateOnboardingCreatePasskey flowpilot.StateName = "onboarding_create_passkey" + StateOnboardingVerifyPasskeyAttestation flowpilot.StateName = "onboarding_verify_passkey_attestation" + + StateCreate2FASecurityKey flowpilot.StateName = "create_2fa_security_key" + StateVerify2FASecurityKeyAssertion flowpilot.StateName = "verify_2fa_security_key_assertion" + StateCreate2FATOTP flowpilot.StateName = "create_2fa_totp" + StateGenerateRecoveryCodes flowpilot.StateName = "generate_recovery_codes" + StateShowRecoveryCodes flowpilot.StateName = "show_recovery_codes" +) diff --git a/backend/flow_api_basic_construct/flows/2fa_creation.go b/backend/flow_api_basic_construct/flows/2fa_creation.go new file mode 100644 index 000000000..b538b8844 --- /dev/null +++ b/backend/flow_api_basic_construct/flows/2fa_creation.go @@ -0,0 +1,17 @@ +package flows + +import ( + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" +) + +func New2FACreationSubFlow() flowpilot.SubFlow { + // TODO: + return flowpilot.NewSubFlow(). + State(common.StateCreate2FASecurityKey). + State(common.StateVerify2FASecurityKeyAssertion). + State(common.StateCreate2FATOTP). + State(common.StateGenerateRecoveryCodes). + State(common.StateShowRecoveryCodes). + MustBuild() +} diff --git a/backend/flow_api_basic_construct/flows/login.go b/backend/flow_api_basic_construct/flows/login.go new file mode 100644 index 000000000..aa82a5ca7 --- /dev/null +++ b/backend/flow_api_basic_construct/flows/login.go @@ -0,0 +1,31 @@ +package flows + +import ( + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/actions" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "time" +) + +func NewLoginFlow(cfg config.Config) flowpilot.Flow { + return flowpilot.NewFlow("/login"). + State(common.StateLoginPreflight, actions.NewSendCapabilities(cfg)). + State(common.StateLoginInit). + State(common.StateLoginMethodChooser). + State(common.StatePasskeyLogin). + State(common.StatePasswordLogin). + State(common.StateRecoveryPasscodeConfirmation). + State(common.StateLoginPasscodeConfirmation). + State(common.StateUse2FASecurityKey). + State(common.StateUse2FATOTP). + State(common.StateUseRecoveryCode). + State(common.StateRecoveryPasswordCreation). + State(common.StateSuccess). + State(common.StateError). + //SubFlows(NewPasskeyOnboardingSubFlow(), New2FACreationSubFlow()). + InitialState(common.StateLoginPreflight). + ErrorState(common.StateError). + TTL(10 * time.Minute). + MustBuild() +} diff --git a/backend/flow_api_basic_construct/flows/passkey_onboarding.go b/backend/flow_api_basic_construct/flows/passkey_onboarding.go new file mode 100644 index 000000000..a25e037c3 --- /dev/null +++ b/backend/flow_api_basic_construct/flows/passkey_onboarding.go @@ -0,0 +1,48 @@ +package flows + +import ( + "github.com/go-webauthn/webauthn/protocol" + webauthnLib "github.com/go-webauthn/webauthn/webauthn" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/actions" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/session" + "time" +) + +func NewPasskeyOnboardingSubFlow(cfg config.Config, persister persistence.Persister, sessionManager session.Manager, httpContext echo.Context) (flowpilot.SubFlow, error) { + // TODO: + f := false + wa, err := webauthnLib.New(&webauthnLib.Config{ + RPID: cfg.Webauthn.RelyingParty.Id, + RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName, + RPOrigins: cfg.Webauthn.RelyingParty.Origins, + AttestationPreference: protocol.PreferNoAttestation, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + RequireResidentKey: &f, + ResidentKey: protocol.ResidentKeyRequirementDiscouraged, + UserVerification: protocol.VerificationRequired, + }, + Debug: false, + Timeouts: webauthnLib.TimeoutsConfig{ + Login: webauthnLib.TimeoutConfig{ + Enforce: true, + Timeout: time.Duration(cfg.Webauthn.Timeout) * time.Millisecond, + }, + Registration: webauthnLib.TimeoutConfig{ + Enforce: true, + Timeout: time.Duration(cfg.Webauthn.Timeout) * time.Millisecond, + }, + }, + }) + if err != nil { + return nil, err + } + return flowpilot.NewSubFlow(). + State(common.StateOnboardingCreatePasskey, actions.NewGetWACreationOptions(cfg, persister, wa), actions.NewSkip(cfg)). + State(common.StateOnboardingVerifyPasskeyAttestation, actions.NewSendWAAttestationResponse(cfg, persister, wa, sessionManager, httpContext)). + Build() +} diff --git a/backend/flow_api_basic_construct/flows/registration.go b/backend/flow_api_basic_construct/flows/registration.go new file mode 100644 index 000000000..fe61f4009 --- /dev/null +++ b/backend/flow_api_basic_construct/flows/registration.go @@ -0,0 +1,36 @@ +package flows + +import ( + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/actions" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/common" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/hooks" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/services" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/session" + "time" +) + +func NewRegistrationFlow(cfg config.Config, persister persistence.Persister, passcodeService services.Passcode, sessionManager session.Manager, httpContext echo.Context) (flowpilot.Flow, error) { + passkeyOnboardingSubFlow, err := NewPasskeyOnboardingSubFlow(cfg, persister, sessionManager, httpContext) + if err != nil { + return nil, err + } + + return flowpilot.NewFlow("/registration"). + State(common.StateRegistrationPreflight, actions.NewSendCapabilities(cfg)). + State(common.StateRegistrationInit, actions.NewSubmitRegistrationIdentifier(cfg, persister, passcodeService, httpContext), actions.NewLoginWithOauth()). + State(common.StateEmailVerification, actions.NewSubmitPasscode(cfg, persister)). + State(common.StatePasswordCreation, actions.NewSubmitNewPassword(cfg)). + BeforeState(common.StateSuccess, hooks.NewBeforeSuccess(persister, sessionManager, httpContext)). + State(common.StateSuccess). + State(common.StateError). + SubFlows(passkeyOnboardingSubFlow). + InitialState(common.StateRegistrationPreflight). + ErrorState(common.StateError). + TTL(10 * time.Minute). + Debug(true). + MustBuild(), nil +} diff --git a/backend/flow_api_basic_construct/handler.go b/backend/flow_api_basic_construct/handler.go new file mode 100644 index 000000000..3502ad7c5 --- /dev/null +++ b/backend/flow_api_basic_construct/handler.go @@ -0,0 +1,77 @@ +package flow_api_basic_construct + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/flows" + "github.com/teamhanko/hanko/backend/flow_api_basic_construct/services" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/session" + "net/http" +) + +func NewHandler(cfg config.Config, persister persistence.Persister, passcodeService services.Passcode, sessionManager session.Manager) *FlowPilotHandler { + return &FlowPilotHandler{ + persister, + cfg, + passcodeService, + sessionManager, + } +} + +type FlowPilotHandler struct { + persister persistence.Persister + cfg config.Config + passcodeService services.Passcode + sessionManager session.Manager +} + +func (h *FlowPilotHandler) RegistrationFlowHandler(c echo.Context) error { + registrationFlow, err := flows.NewRegistrationFlow(h.cfg, h.persister, h.passcodeService, h.sessionManager, c) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf("failed to create registration flow: %w", err)) + } + + return h.executeFlow(c, registrationFlow) +} + +func (h *FlowPilotHandler) LoginFlowHandler(c echo.Context) error { + loginFlow := flows.NewLoginFlow(h.cfg) + + return h.executeFlow(c, loginFlow) +} + +func (h *FlowPilotHandler) executeFlow(c echo.Context, flow flowpilot.Flow) error { + actionParam := c.QueryParam("flowpilot_action") + + var body flowpilot.InputData + err := c.Bind(&body) + if err != nil { + result := flow.ResultFromError(flowpilot.ErrorTechnical.Wrap(err)) + return c.JSON(result.Status(), result.Response()) + } + + err = h.persister.Transaction(func(tx *pop.Connection) error { + db := models.NewFlowDB(tx) + + result, flowPilotErr := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(body)) + if flowPilotErr != nil { + return flowPilotErr + } + + return c.JSON(result.Status(), result.Response()) + }) + + if err != nil { + c.Logger().Errorf("tx error: %v", err) + result := flow.ResultFromError(err) + + return c.JSON(result.Status(), result.Response()) + } + + return nil // TODO: maybe return TechnicalError or something else +} diff --git a/backend/flow_api_basic_construct/hooks/before_success.go b/backend/flow_api_basic_construct/hooks/before_success.go new file mode 100644 index 000000000..cd08452f0 --- /dev/null +++ b/backend/flow_api_basic_construct/hooks/before_success.go @@ -0,0 +1,134 @@ +package hooks + +import ( + "encoding/json" + webauthnLib "github.com/go-webauthn/webauthn/webauthn" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/dto/intern" + "github.com/teamhanko/hanko/backend/flowpilot" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" + "github.com/teamhanko/hanko/backend/session" + "time" +) + +func NewBeforeSuccess(persister persistence.Persister, sessionManager session.Manager, httpContext echo.Context) BeforeSuccess { + return BeforeSuccess{ + persister, + sessionManager, + httpContext, + } +} + +type BeforeSuccess struct { + persister persistence.Persister + sessionManager session.Manager + httpContext echo.Context +} + +func (m BeforeSuccess) Execute(c flowpilot.HookExecutionContext) error { + userId, err := uuid.NewV4() + if err != nil { + return err + } + if c.Stash().Get("user_id").Exists() { + userId, err = uuid.FromString(c.Stash().Get("user_id").String()) + if err != nil { + return err + } + } + + passkeyCredentialStr := c.Stash().Get("passkey_credential").String() + var passkeyCredential webauthnLib.Credential + err = json.Unmarshal([]byte(passkeyCredentialStr), &passkeyCredential) + if err != nil { + return err + } + passkeyBackupEligible := c.Stash().Get("passkey_backup_eligible").Bool() + passkeyBackupState := c.Stash().Get("passkey_backup_state").Bool() + + credentialModel := intern.WebauthnCredentialToModel(&passkeyCredential, userId, passkeyBackupEligible, passkeyBackupState) + err = m.CreateUser( + userId, + c.Stash().Get("email").String(), + c.Stash().Get("email_verified").Bool(), + c.Stash().Get("username").String(), + credentialModel, + c.Stash().Get("new_password").String(), + ) + if err != nil { + return err + } + + sessionToken, err := m.sessionManager.GenerateJWT(userId) + if err != nil { + return err + } + cookie, err := m.sessionManager.GenerateCookie(sessionToken) + if err != nil { + return err + } + + m.httpContext.SetCookie(cookie) + + return nil +} + +func (m BeforeSuccess) CreateUser(id uuid.UUID, email string, emailVerified bool, username string, passkey *models.WebauthnCredential, password string) error { + return m.persister.Transaction(func(tx *pop.Connection) error { + // TODO: add audit log + now := time.Now().UTC() + err := m.persister.GetUserPersisterWithConnection(tx).Create(models.User{ + ID: id, + CreatedAt: now, + UpdatedAt: now, + }) + if err != nil { + return err + } + + if email != "" { + emailModel := models.NewEmail(&id, email) + emailModel.Verified = emailVerified + err = m.persister.GetEmailPersisterWithConnection(tx).Create(*emailModel) + if err != nil { + return err + } + + primaryEmail := models.NewPrimaryEmail(emailModel.ID, id) + err = m.persister.GetPrimaryEmailPersisterWithConnection(tx).Create(*primaryEmail) + if err != nil { + return err + } + } + + if username != "" { + usernameModel := models.NewUsername(id, username) + err = m.persister.GetUsernamePersisterWithConnection(tx).Create(*usernameModel) + if err != nil { + return err + } + } + + if passkey != nil { + err = m.persister.GetWebauthnCredentialPersisterWithConnection(tx).Create(*passkey) + if err != nil { + return err + } + } + + if password != "" { + err = m.persister.GetPasswordCredentialPersisterWithConnection(tx).Create(models.PasswordCredential{ + UserId: id, + Password: password, + }) + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/backend/flow_api_basic_construct/services/email.go b/backend/flow_api_basic_construct/services/email.go new file mode 100644 index 000000000..61852df91 --- /dev/null +++ b/backend/flow_api_basic_construct/services/email.go @@ -0,0 +1,62 @@ +package services + +import ( + "fmt" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/mail" + "gopkg.in/gomail.v2" +) + +type Email struct { + renderer *mail.Renderer + mailer mail.Mailer + cfg config.Config +} + +func NewEmailService(cfg config.Config) (*Email, error) { + renderer, err := mail.NewRenderer() + if err != nil { + return nil, err + } + mailer, err := mail.NewMailer(cfg.Passcode.Smtp) + if err != nil { + panic(fmt.Errorf("failed to create mailer: %w", err)) + } + + return &Email{ + renderer, + mailer, + cfg, + }, nil +} + +// SendEmail sends an email with a translated specified template as body. +// The template name must be the name of the template without the content type and the file ending. +// E.g. when the file is created as "email_verification_text.tmpl" then the template name is just "email_verification" +// Currently only "[template_name]_text.tmpl" template can be used. +// The subject header of an email is also translated. The message_key must be "subject_[template_name]". +func (service *Email) SendEmail(template string, lang string, data map[string]interface{}, emailAddress string) error { + text, err := service.renderer.Render(fmt.Sprintf("%s_text.tmpl", template), lang, data) + if err != nil { + return err + } + //html, err := service.renderer.Render(fmt.Sprintf("%s_html.tmpl", template), lang, data) + if err != nil { + return err + } + + message := gomail.NewMessage() + message.SetAddressHeader("To", emailAddress, "") + message.SetAddressHeader("From", service.cfg.Passcode.Email.FromAddress, service.cfg.Passcode.Email.FromName) + + message.SetHeader("Subject", service.renderer.Translate(lang, fmt.Sprintf("subject_%s", template), data)) + message.SetBody("text/plain", text) + //message.AddAlternative("text/html", html) + + err = service.mailer.Send(message) + if err != nil { + return err + } + + return nil +} diff --git a/backend/flow_api_basic_construct/services/passcode.go b/backend/flow_api_basic_construct/services/passcode.go new file mode 100644 index 000000000..363fb7d25 --- /dev/null +++ b/backend/flow_api_basic_construct/services/passcode.go @@ -0,0 +1,92 @@ +package services + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/crypto" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" + "golang.org/x/crypto/bcrypt" + "time" +) + +type Passcode interface { + SendEmailVerification(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) + SendLogin(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) + PasswordRecovery(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) +} + +type passcode struct { + emailService Email + passcodeGenerator crypto.PasscodeGenerator + persister persistence.Persister + cfg config.Config +} + +func NewPasscodeService(cfg config.Config, emailService Email, persister persistence.Persister) Passcode { + return &passcode{ + emailService, + crypto.NewPasscodeGenerator(), + persister, + cfg, + } +} + +func (service *passcode) SendEmailVerification(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return service.sendPasscode(flowID, "email_verification", emailAddress, lang) +} + +func (service *passcode) SendLogin(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return service.sendPasscode(flowID, "login", emailAddress, lang) +} + +func (service *passcode) PasswordRecovery(flowID uuid.UUID, emailAddress string, lang string) (uuid.UUID, error) { + return service.sendPasscode(flowID, "password_recovery", emailAddress, lang) +} + +func (service *passcode) sendPasscode(flowID uuid.UUID, template string, emailAddress string, lang string) (uuid.UUID, error) { + code, err := service.passcodeGenerator.Generate() + if err != nil { + return uuid.Nil, err + } + hashedPasscode, err := bcrypt.GenerateFromPassword([]byte(code), 12) + if err != nil { + return uuid.Nil, err + } + + passcodeId, err := uuid.NewV4() + if err != nil { + return uuid.Nil, err + } + + now := time.Now().UTC() + passcodeModel := models.Passcode{ + ID: passcodeId, + FlowID: &flowID, + Ttl: service.cfg.Passcode.TTL, + Code: string(hashedPasscode), + TryCount: 0, + CreatedAt: now, + UpdatedAt: now, + } + + err = service.persister.GetPasscodePersister().Create(passcodeModel) + if err != nil { + return uuid.Nil, err + } + + durationTTL := time.Duration(passcodeModel.Ttl) * time.Second + data := map[string]interface{}{ + "Code": code, + "ServiceName": service.cfg.Service.Name, + "TTL": fmt.Sprintf("%.0f", durationTTL.Minutes()), + } + + err = service.emailService.SendEmail(template, lang, data, emailAddress) + if err != nil { + return uuid.Nil, err + } + + return passcodeId, nil +} diff --git a/backend/flow_api_test/static/generic_client.html b/backend/flow_api_test/static/generic_client.html index a2988a951..434e5ab29 100644 --- a/backend/flow_api_test/static/generic_client.html +++ b/backend/flow_api_test/static/generic_client.html @@ -46,15 +46,39 @@

✨ Login

- + - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

AuthFlowCompletedDetail

-
- - - - - -
- -
- -

AuthFlowCompletedDetail

- - -
- -
-
- - -
The data passed in the `hanko-auth-flow-completed` event.
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The user associated with the removed session.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/CustomEvents.ts, line 65 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Client.html b/docs/static/jsdoc/hanko-frontend-sdk/Client.html index a94ac628e..8f8d13e30 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Client.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Client.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Config.html b/docs/static/jsdoc/hanko-frontend-sdk/Config.html index d2b81c635..0ae4ff749 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Config.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Config.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ConfigClient.html b/docs/static/jsdoc/hanko-frontend-sdk/ConfigClient.html deleted file mode 100644 index 477c9e09b..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/ConfigClient.html +++ /dev/null @@ -1,523 +0,0 @@ - - - - - - - - ConfigClient - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

ConfigClient

-
- - - - - -
- -
- -

ConfigClient()

- -
A class for retrieving configurations from the API.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new ConfigClient() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/ConfigClient.ts, line 11 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -HttpClient - - - - -

- # - - - client - - -

- - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/Client.ts, line 20 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - async - - - - - get() → {Promise.<Config>} - - -

- - - - -
- Retrieves the frontend configuration. -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/ConfigClient.ts, line 42 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<Config> - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ConflictError.html b/docs/static/jsdoc/hanko-frontend-sdk/ConflictError.html index 290110a4a..b145c3d9a 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/ConflictError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/ConflictError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Cookie.html b/docs/static/jsdoc/hanko-frontend-sdk/Cookie.html index 0a83c5347..c55a8bc7d 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Cookie.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Cookie.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/CookieOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/CookieOptions.html index 8ebeb664d..4ac2273fe 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/CookieOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/CookieOptions.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Credential.html b/docs/static/jsdoc/hanko-frontend-sdk/Credential.html index 2bb4c712f..c9ca25b98 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Credential.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Credential.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Dispatcher.html b/docs/static/jsdoc/hanko-frontend-sdk/Dispatcher.html index 9ef32ab74..0c9c477fc 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Dispatcher.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Dispatcher.html @@ -66,7 +66,7 @@ @@ -282,154 +282,6 @@

Methods

-

- # - - - - dispatchAuthFlowCompletedEvent(detail) - - -

- - - - -
- Dispatches a "hanko-auth-flow-completed" event to the document with the specified detail. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
detail - - -AuthFlowCompletedDetail - - - - The event detail.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/Dispatcher.ts, line 119 - -

- -
- - - - - - - - - - - - - - - - - - - - - - - -
- - -

# @@ -546,7 +398,7 @@

Parameters:

View Source - lib/events/Dispatcher.ts, line 93 + lib/events/Dispatcher.ts, line 84

@@ -643,7 +495,7 @@

View Source - lib/events/Dispatcher.ts, line 99 + lib/events/Dispatcher.ts, line 90

@@ -740,7 +592,7 @@

View Source - lib/events/Dispatcher.ts, line 111 + lib/events/Dispatcher.ts, line 102

@@ -837,7 +689,7 @@

View Source - lib/events/Dispatcher.ts, line 105 + lib/events/Dispatcher.ts, line 96

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/DispatcherOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/DispatcherOptions.html index a056169ca..79c7de0a4 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/DispatcherOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/DispatcherOptions.html @@ -66,7 +66,7 @@

@@ -187,7 +187,7 @@
Properties:

View Source - lib/events/Dispatcher.ts, line 67 + lib/events/Dispatcher.ts, line 58

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Email.html b/docs/static/jsdoc/hanko-frontend-sdk/Email.html index a3987c45e..2ff4aa242 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Email.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Email.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/EmailAddressAlreadyExistsError.html b/docs/static/jsdoc/hanko-frontend-sdk/EmailAddressAlreadyExistsError.html index 6c6367c7b..c76169706 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/EmailAddressAlreadyExistsError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/EmailAddressAlreadyExistsError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/EmailClient.html b/docs/static/jsdoc/hanko-frontend-sdk/EmailClient.html index c27a87185..c2dbe5745 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/EmailClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/EmailClient.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/EmailConfig.html b/docs/static/jsdoc/hanko-frontend-sdk/EmailConfig.html index 71b6089f1..25e63bfce 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/EmailConfig.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/EmailConfig.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Emails.html b/docs/static/jsdoc/hanko-frontend-sdk/Emails.html index 3169e6d23..f21503e67 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Emails.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Emails.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/EnterpriseClient.html b/docs/static/jsdoc/hanko-frontend-sdk/EnterpriseClient.html index d72860934..cfab0e9b8 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/EnterpriseClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/EnterpriseClient.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ForbiddenError.html b/docs/static/jsdoc/hanko-frontend-sdk/ForbiddenError.html index 25ba3d80b..590eae5ac 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/ForbiddenError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/ForbiddenError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Hanko.html b/docs/static/jsdoc/hanko-frontend-sdk/Hanko.html index f3a337115..6acb13568 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Hanko.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Hanko.html @@ -66,7 +66,7 @@ @@ -263,7 +263,7 @@
Parameters:

View Source - Hanko.ts, line 21 + Hanko.ts, line 18

@@ -330,79 +330,6 @@

Members

-ConfigClient - - - - -

- # - - - config - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - Hanko.ts, line 62 - -

- -
- - - - - - - -
- - - - EmailClient @@ -459,7 +386,7 @@

View Source - Hanko.ts, line 87 + Hanko.ts, line 61

@@ -532,80 +459,7 @@

View Source - Hanko.ts, line 97 - -

- - - - - - - -

- -
- - - - -PasscodeClient - - - - -

- # - - - passcode - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - Hanko.ts, line 82 + Hanko.ts, line 71

@@ -622,16 +476,16 @@

-PasswordClient +Flow -

- # +

+ # - password + flow

@@ -678,7 +532,7 @@

View Source - Hanko.ts, line 77 + Hanko.ts, line 91

@@ -751,7 +605,7 @@

View Source - Hanko.ts, line 107 + Hanko.ts, line 81

@@ -824,7 +678,7 @@

View Source - Hanko.ts, line 112 + Hanko.ts, line 86

@@ -897,7 +751,7 @@

View Source - Hanko.ts, line 92 + Hanko.ts, line 66

@@ -970,7 +824,7 @@

View Source - Hanko.ts, line 102 + Hanko.ts, line 76

@@ -1043,80 +897,7 @@

View Source - Hanko.ts, line 67 - -

- -

- - - - - -
- -
- - - - -WebauthnClient - - - - -

- # - - - webauthn - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - Hanko.ts, line 72 + Hanko.ts, line 56

@@ -1141,229 +922,6 @@

Methods

-

- # - - - - onAuthFlowCompleted(callback, onceopt) → {CleanupFunc} - - -

- - - - -
- Adds an event listener for hanko-auth-flow-completed events. Will be triggered after the login or registration flow has been completed. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
callback - - -CallbackFunc.<AuthFlowCompletedDetail> - - - - - - - - - - The function to be called when the event is triggered.
once - - -boolean - - - - - - <optional>
- - - - - -
Whether the event listener should be removed after being called once.
-
- - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/Listener.ts, line 131 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- -
This function can be called to remove the event listener.
- - -
- - -CleanupFunc - - -
- -
- - -
-
- - - - -
- -
- - -

# diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Hanko.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/Hanko.ts.html index 76b874259..a42fd2e67 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Hanko.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Hanko.ts.html @@ -68,7 +68,7 @@

@@ -85,12 +85,8 @@

Hanko.ts

-
import { ConfigClient } from "./lib/client/ConfigClient";
-import { EnterpriseClient } from "./lib/client/EnterpriseClient";
-import { PasscodeClient } from "./lib/client/PasscodeClient";
-import { PasswordClient } from "./lib/client/PasswordClient";
+            
import { EnterpriseClient } from "./lib/client/EnterpriseClient";
 import { UserClient } from "./lib/client/UserClient";
-import { WebauthnClient } from "./lib/client/WebauthnClient";
 import { EmailClient } from "./lib/client/EmailClient";
 import { ThirdPartyClient } from "./lib/client/ThirdPartyClient";
 import { TokenClient } from "./lib/client/TokenClient";
@@ -98,6 +94,7 @@ 

Hanko.ts

import { Relay } from "./lib/events/Relay"; import { Session } from "./lib/Session"; import { CookieSameSite } from "./lib/Cookie"; +import { Flow } from "./lib/flow-api/Flow"; /** * The options for the Hanko class @@ -126,17 +123,14 @@

Hanko.ts

*/ class Hanko extends Listener { api: string; - config: ConfigClient; user: UserClient; - webauthn: WebauthnClient; - password: PasswordClient; - passcode: PasscodeClient; email: EmailClient; thirdParty: ThirdPartyClient; enterprise: EnterpriseClient; token: TokenClient; relay: Relay; session: Session; + flow: Flow; // eslint-disable-next-line require-jsdoc constructor(api: string, options?: HankoOptions) { @@ -163,31 +157,11 @@

Hanko.ts

} this.api = api; - /** - * @public - * @type {ConfigClient} - */ - this.config = new ConfigClient(api, opts); /** * @public * @type {UserClient} */ this.user = new UserClient(api, opts); - /** - * @public - * @type {WebauthnClient} - */ - this.webauthn = new WebauthnClient(api, opts); - /** - * @public - * @type {PasswordClient} - */ - this.password = new PasswordClient(api, opts); - /** - * @public - * @type {PasscodeClient} - */ - this.passcode = new PasscodeClient(api, opts); /** * @public * @type {EmailClient} @@ -218,6 +192,11 @@

Hanko.ts

* @type {Session} */ this.session = new Session({ ...opts }); + /** + * @public + * @type {Flow} + */ + this.flow = new Flow(api, opts); } } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/HankoError.html b/docs/static/jsdoc/hanko-frontend-sdk/HankoError.html index cd5e991b7..cb286ff11 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/HankoError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/HankoError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/HankoOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/HankoOptions.html index 8b3839ab9..0cf08b67a 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/HankoOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/HankoOptions.html @@ -66,7 +66,7 @@ @@ -321,7 +321,7 @@
Properties:

View Source - Hanko.ts, line 130 + Hanko.ts, line 106

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Headers.html b/docs/static/jsdoc/hanko-frontend-sdk/Headers.html index 3fb4b5912..e0ccd1ddb 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Headers.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Headers.html @@ -66,7 +66,7 @@ @@ -225,7 +225,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 15 + lib/client/HttpClient.ts, line 14

@@ -398,7 +398,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 287 + lib/client/HttpClient.ts, line 275

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html b/docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html index 89e9d6e6d..a91e55a5a 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html @@ -66,7 +66,7 @@ @@ -249,7 +249,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 117 + lib/client/HttpClient.ts, line 116

@@ -422,7 +422,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 378 + lib/client/HttpClient.ts, line 364

@@ -630,7 +630,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 335 + lib/client/HttpClient.ts, line 321

@@ -883,7 +883,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 368 + lib/client/HttpClient.ts, line 354

@@ -1136,7 +1136,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 346 + lib/client/HttpClient.ts, line 332

@@ -1228,12 +1228,12 @@
Parameters:
-

- # +

+ # - processResponseHeadersOnLogin(userID, response) + processHeaders(xhr)

@@ -1242,8 +1242,7 @@

- Processes the response headers on login and extracts the JWT and expiration time. Also, the passcode state will be -removed, the session state updated und a `hanko-session-created` event will be dispatched. + Processes the response headers on login and extracts the JWT and expiration time.
@@ -1281,38 +1280,13 @@

Parameters:
- userID + xhr -string - - - - - - - - - - The user ID. - - - - - - - - - response - - - - - -Response +XMLHttpRequest @@ -1322,7 +1296,7 @@
Parameters:
- The HTTP response object. + The xhr object. @@ -1370,7 +1344,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 325 + lib/client/HttpClient.ts, line 311

@@ -1563,7 +1537,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 357 + lib/client/HttpClient.ts, line 343

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/HttpClientOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/HttpClientOptions.html index 8c2ab2c22..eb321a416 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/HttpClientOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/HttpClientOptions.html @@ -66,7 +66,7 @@ @@ -284,7 +284,7 @@
Properties:

View Source - lib/client/HttpClient.ts, line 305 + lib/client/HttpClient.ts, line 293

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Identity.html b/docs/static/jsdoc/hanko-frontend-sdk/Identity.html index 19efd56fc..8658dcb47 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Identity.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Identity.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasscodeError.html b/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasscodeError.html index d72e7c899..ca43adbb8 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasscodeError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasscodeError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasswordError.html b/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasswordError.html index 51b22d374..6527a8fd0 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasswordError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/InvalidPasswordError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/InvalidWebauthnCredentialError.html b/docs/static/jsdoc/hanko-frontend-sdk/InvalidWebauthnCredentialError.html index 35f5d336b..a15ceffdf 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/InvalidWebauthnCredentialError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/InvalidWebauthnCredentialError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Listener.html b/docs/static/jsdoc/hanko-frontend-sdk/Listener.html index 5b916451e..8528aa0f6 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Listener.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Listener.html @@ -66,7 +66,7 @@ @@ -224,12 +224,12 @@

Methods

-

- # +

+ # - onAuthFlowCompleted(callback, onceopt) → {CleanupFunc} + onSessionCreated(callback, onceopt) → {CleanupFunc}

@@ -238,7 +238,8 @@

- Adds an event listener for hanko-auth-flow-completed events. Will be triggered after the login or registration flow has been completed. + Adds an event listener for "hanko-session-created" events. Will be triggered across all browser windows, when the user +logs in, or when the page has been loaded or refreshed and there is a valid session.
@@ -284,7 +285,7 @@

Parameters:
-CallbackFunc.<AuthFlowCompletedDetail> +CallbackFunc.<SessionDetail> @@ -385,7 +386,7 @@
Parameters:

View Source - lib/events/Listener.ts, line 280 + lib/events/Listener.ts, line 227

@@ -442,12 +443,12 @@
Parameters:
-

- # +

+ # - onSessionCreated(callback, onceopt) → {CleanupFunc} + onSessionExpired(callback, onceopt) → {CleanupFunc}

@@ -456,8 +457,9 @@

- Adds an event listener for "hanko-session-created" events. Will be triggered across all browser windows, when the user -logs in, or when the page has been loaded or refreshed and there is a valid session. + Adds an event listener for "hanko-session-expired" events. The event will be triggered across all browser windows +as soon as the current JWT expires or the user logs out. It also triggers, when the user deletes the account in +another window.
@@ -503,7 +505,7 @@

Parameters:
-CallbackFunc.<SessionDetail> +CallbackFunc.<null> @@ -604,7 +606,7 @@
Parameters:

View Source - lib/events/Listener.ts, line 237 + lib/events/Listener.ts, line 239

@@ -661,12 +663,12 @@
Parameters:
-

- # +

+ # - onSessionExpired(callback, onceopt) → {CleanupFunc} + onUserDeleted(callback, onceopt) → {CleanupFunc}

@@ -675,9 +677,7 @@

- Adds an event listener for "hanko-session-expired" events. The event will be triggered across all browser windows -as soon as the current JWT expires or the user logs out. It also triggers, when the user deletes the account in -another window. + Adds an event listener for hanko-user-deleted events. The event triggers, when the user has deleted the account.
@@ -824,7 +824,7 @@

Parameters:

View Source - lib/events/Listener.ts, line 249 + lib/events/Listener.ts, line 260

@@ -881,12 +881,12 @@
Parameters:
-

- # +

+ # - onUserDeleted(callback, onceopt) → {CleanupFunc} + onUserLoggedOut(callback, onceopt) → {CleanupFunc}

@@ -895,7 +895,8 @@

- Adds an event listener for hanko-user-deleted events. The event triggers, when the user has deleted the account. + Adds an event listener for hanko-user-deleted events. The event triggers, when the user has deleted the account in +the browser window where the deletion happened.
@@ -1042,7 +1043,7 @@

Parameters:

View Source - lib/events/Listener.ts, line 270 + lib/events/Listener.ts, line 250

@@ -1095,16 +1096,25 @@
Parameters:
+ + + + + +
+

Type Definitions

+
+
-

- # +

+ # - onUserLoggedOut(callback, onceopt) → {CleanupFunc} + CallbackFunc(detail)

@@ -1113,8 +1123,7 @@

- Adds an event listener for hanko-user-deleted events. The event triggers, when the user has deleted the account in -the browser window where the deletion happened. + A callback function to be executed when an event is triggered.
@@ -1139,8 +1148,6 @@

Parameters:
Type - Attributes - @@ -1154,66 +1161,23 @@
Parameters:
- callback - - - - - -CallbackFunc.<null> - - - - - - - - - - - - - - - - - - The function to be called when the event is triggered. - - - - - - - - - once + detail -boolean +T - - - <optional>
- - - - - - - - Whether the event listener should be removed after being called once. + @@ -1261,7 +1225,7 @@
Parameters:

View Source - lib/events/Listener.ts, line 260 + lib/events/Listener.ts, line 128

@@ -1284,55 +1248,21 @@
Parameters:
-
-
-
- - -
- -
This function can be called to remove the event listener.
- - -
- - -CleanupFunc -
-
- - -
-
- - - - -
- -
-
- - - -
-

Type Definitions

-
-

- # +

+ # - CallbackFunc(detail) + CleanupFunc()

@@ -1341,7 +1271,8 @@

- A callback function to be executed when an event is triggered. + A function returned when adding an event listener. The function can be called to remove the corresponding event +listener.
@@ -1353,57 +1284,6 @@

-

Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
detail - - -T - - - -
-
- @@ -1443,7 +1323,7 @@
Parameters:

View Source - lib/events/Listener.ts, line 138 + lib/events/Listener.ts, line 145

@@ -1473,34 +1353,29 @@
Parameters:
- - -

- # + + - - - CleanupFunc() - - -

- - - - -
- A function returned when adding an event listener. The function can be called to remove the corresponding event -listener. -
- - +string + +

+ # + + + sessionCreatedType + + +

+
+ The type of the `hanko-session-created` event. +
@@ -1539,9 +1414,9 @@

- View Source + View Source - lib/events/Listener.ts, line 155 + lib/events/CustomEvents.ts, line 2

@@ -1550,22 +1425,6 @@

- - - - - - - - - - - - - - - -

@@ -1579,11 +1438,11 @@

-

- # +

+ # - authFlowCompletedType + sessionExpiredType

@@ -1592,7 +1451,7 @@

- The type of the `hanko-auth-flow-completed` event. + The type of the `hanko-session-expired` event.
@@ -1634,7 +1493,7 @@

View Source - lib/events/CustomEvents.ts, line 26 + lib/events/CustomEvents.ts, line 8

@@ -1656,11 +1515,11 @@

-

- # +

+ # - sessionCreatedType + userCreatedType

@@ -1669,7 +1528,7 @@

- The type of the `hanko-session-created` event. + The type of the `hanko-user-created` event.
@@ -1711,7 +1570,7 @@

View Source - lib/events/CustomEvents.ts, line 2 + lib/events/CustomEvents.ts, line 32

@@ -1733,11 +1592,11 @@

-

- # +

+ # - sessionExpiredType + userDeletedType

@@ -1746,7 +1605,7 @@

- The type of the `hanko-session-expired` event. + The type of the `hanko-user-deleted` event.
@@ -1788,7 +1647,7 @@

View Source - lib/events/CustomEvents.ts, line 8 + lib/events/CustomEvents.ts, line 20

@@ -1810,11 +1669,11 @@

-

- # +

+ # - userDeletedType + userLoggedInType

@@ -1823,7 +1682,7 @@

- The type of the `hanko-user-deleted` event. + The type of the `hanko-user-logged-in` event.
@@ -1865,7 +1724,7 @@

View Source - lib/events/CustomEvents.ts, line 20 + lib/events/CustomEvents.ts, line 26

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorage.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorage.html index 254b8cb6d..f77821849 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorage.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorage.html @@ -66,7 +66,7 @@

@@ -132,7 +132,7 @@
Properties:
-LocalStorageUsers +LocalStorageUsers @@ -195,7 +195,7 @@
Properties:

View Source - lib/state/State.ts, line 80 + lib/state/State.ts, line 79

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePasscode.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePasscode.html deleted file mode 100644 index e8adb8311..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePasscode.html +++ /dev/null @@ -1,350 +0,0 @@ - - - - - - - - LocalStoragePasscode - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

LocalStoragePasscode

-
- - - - - -
- -
- -

LocalStoragePasscode

- - -
- -
-
- - - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
id - - -string - - - - - - <optional>
- - - -
The UUID of the active passcode.
ttl - - -number - - - - - - <optional>
- - - -
Timestamp until when the passcode is valid in seconds (since January 1, 1970 00:00:00 UTC).
resendAfter - - -number - - - - - - <optional>
- - - -
Seconds until a passcode can be resent.
emailID - - -emailID - - - - - - <optional>
- - - -
The email address ID.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 131 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePassword.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePassword.html deleted file mode 100644 index 57827f977..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStoragePassword.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - LocalStoragePassword - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

LocalStoragePassword

-
- - - - - -
- -
- -

LocalStoragePassword

- - -
- -
-
- - - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
retryAfter - - -number - - - - - - <optional>
- - - -
Timestamp (in seconds since January 1, 1970 00:00:00 UTC) indicating when the next password login can be attempted.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasswordState.ts, line 57 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageSession.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageSession.html index cb02b1d2e..cf2b90b78 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageSession.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageSession.html @@ -66,7 +66,7 @@
@@ -181,7 +181,7 @@
Properties:

View Source - lib/state/session/SessionState.ts, line 108 + lib/state/session/SessionState.ts, line 91

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUser.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUser.html deleted file mode 100644 index f81e974be..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUser.html +++ /dev/null @@ -1,319 +0,0 @@ - - - - - - - - LocalStorageUser - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

LocalStorageUser

-
- - - - - -
- -
- -

LocalStorageUser

- - -
- -
-
- - - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
webauthn - - -LocalStorageWebauthn - - - - - - <optional>
- - - -
Information about WebAuthn credentials.
passcode - - -LocalStoragePasscode - - - - - - <optional>
- - - -
Information about the active passcode.
password - - -LocalStoragePassword - - - - - - <optional>
- - - -
Information about the password login attempts.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 39 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUsers.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUsers.html deleted file mode 100644 index e05866716..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageUsers.html +++ /dev/null @@ -1,243 +0,0 @@ - - - - - - - - LocalStorageUsers - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

LocalStorageUsers

-
- - - - - -
- -
- -

LocalStorageUsers

- - -
- -
-
- - - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
- - -Object.<string, LocalStorageUser> - - - - A dictionary for mapping users to their states.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 48 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageWebauthn.html b/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageWebauthn.html deleted file mode 100644 index 261c738cf..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/LocalStorageWebauthn.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - LocalStorageWebauthn - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

LocalStorageWebauthn

-
- - - - - -
- -
- -

LocalStorageWebauthn

- - -
- -
-
- - - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
credentials - - -Array.<string> - - - - - - - - <nullable>
- -
A list of known credential IDs on the current browser.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 69 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfEmailAddressesReachedError.html b/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfEmailAddressesReachedError.html index 88d94759c..2bd64798a 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfEmailAddressesReachedError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfEmailAddressesReachedError.html @@ -66,7 +66,7 @@
diff --git a/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfPasscodeAttemptsReachedError.html b/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfPasscodeAttemptsReachedError.html index c41d28d72..8339a5abe 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfPasscodeAttemptsReachedError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/MaxNumOfPasscodeAttemptsReachedError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/NotFoundError.html b/docs/static/jsdoc/hanko-frontend-sdk/NotFoundError.html index ed6970d58..850f51cf7 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/NotFoundError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/NotFoundError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Options.html b/docs/static/jsdoc/hanko-frontend-sdk/Options.html deleted file mode 100644 index e5177e1c7..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/Options.html +++ /dev/null @@ -1,1289 +0,0 @@ - - - - - - - - Options - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

Options

-
- - - - - -
- -
- -

Options

- - -
- -
-
- - -
The options for the Hanko class
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
timeout - - -number - - - - - - <optional>
- - - -
The http request timeout in milliseconds. Defaults to 13000ms
cookieName - - -string - - - - - - <optional>
- - - -
The name of the session cookie set from the SDK. Defaults to "hanko"
storageKey - - -string - - - - - - <optional>
- - - -
The prefix / name of the local storage keys. Defaults to "hanko"
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - Hanko.ts, line 115 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for Cookie
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cookieName - - -string - - - - The name of the session cookie set from the SDK.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/Cookie.ts, line 41 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for Session
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cookieName - - -string - - - - The name of the session cookie set from the SDK.
storageKey - - -string - - - - The prefix / name of the local storage keys.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/Session.ts, line 77 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for the HttpClient
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
timeout - - -number - - - - The http request timeout in milliseconds.
cookieName - - -string - - - - The name of the session cookie set from the SDK.
storageKey - - -string - - - - The prefix / name of the local storage keys.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/HttpClient.ts, line 303 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for Dispatcher
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
storageKey - - -string - - - - The prefix / name of the local storage keys.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/Dispatcher.ts, line 66 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for Relay
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
cookieName - - -string - - - - The name of the session cookie set from the SDK.
storageKey - - -string - - - - The prefix / name of the local storage keys.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/Relay.ts, line 90 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - -
- -
- -

Options

- - -
- -
-
- - -
Options for SessionState
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
storageKey - - -string - - - - The prefix / name of the local storage keys.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/session/SessionState.ts, line 100 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Passcode.html b/docs/static/jsdoc/hanko-frontend-sdk/Passcode.html index 89f6565c1..e07207e85 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Passcode.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Passcode.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeClient.html b/docs/static/jsdoc/hanko-frontend-sdk/PasscodeClient.html deleted file mode 100644 index 19c737053..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeClient.html +++ /dev/null @@ -1,1358 +0,0 @@ - - - - - - - - PasscodeClient - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

PasscodeClient

-
- - - - - -
- -
- -

PasscodeClient()

- -
A class to handle passcodes.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new PasscodeClient() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 13 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -HttpClient - - - - -

- # - - - client - - -

- - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/Client.ts, line 20 - -

- -
- - - - - -
- -
- - - - -PasscodeState - - - - -

- # - - - state - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 22 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - async - - - - - finalize(userID, code) → {Promise.<void>} - - -

- - - - -
- Validates the passcode obtained from the email. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
code - - -string - - - - The passcode digests.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 163 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -InvalidPasscodeError - - -
- - -
- - - -
- - - - - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getResendAfter(userID) → {number} - - -

- - - - -
- Returns the number of seconds the rate limiting is active for. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 179 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getTTL(userID) → {number} - - -

- - - - -
- Returns the number of seconds the current passcode is active for. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 171 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - initialize(userID, emailIDopt, forceopt) → {Promise.<Passcode>} - - -

- - - - -
- Causes the API to send a new passcode to the user's email address. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
userID - - -string - - - - - - - - - - The UUID of the user.
emailID - - -string - - - - - - <optional>
- - - - - -
The UUID of the email address. If unspecified, the email will be sent to the primary email address.
force - - -boolean - - - - - - <optional>
- - - - - -
Indicates the passcode should be sent, even if there is another active passcode.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/PasscodeClient.ts, line 148 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - - -
- - -
- -TooManyRequestsError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<Passcode> - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeExpiredError.html b/docs/static/jsdoc/hanko-frontend-sdk/PasscodeExpiredError.html index 7f2412fed..edff42743 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeExpiredError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/PasscodeExpiredError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeState.html b/docs/static/jsdoc/hanko-frontend-sdk/PasscodeState.html deleted file mode 100644 index 8a451b21f..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasscodeState.html +++ /dev/null @@ -1,2420 +0,0 @@ - - - - - - - - PasscodeState - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

PasscodeState

-
- - - - - -
- -
- -

PasscodeState()

- -
A class that manages passcodes via local storage.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new PasscodeState() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 11 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -LocalStorage - - - - -

- # - - - ls - - -

- - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 23 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - - getActiveID(userID) → {string} - - -

- - - - -
- Gets the UUID of the active passcode. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 167 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -string - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getEmailID(userID) → {string} - - -

- - - - -
- Gets the UUID of the email address. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 184 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -string - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getResendAfter(userID) → {number} - - -

- - - - -
- Gets the number of seconds until when the next passcode can be sent. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 226 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getTTL(userID) → {number} - - -

- - - - -
- Gets the TTL in seconds. When the seconds expire, the code is invalid. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 209 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getUserState(userID) → {LocalStorageUser} - - -

- - - - -
- Gets the state of the specified user. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 25 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -LocalStorageUser - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - read() → {PasscodeState} - - -

- - - - -
- Reads the current state. -
- - - - - - - - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 159 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - reset(userID) → {PasscodeState} - - -

- - - - -
- Removes the active passcode. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 201 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setActiveID(userID, passcodeID) → {PasscodeState} - - -

- - - - -
- Sets the UUID of the active passcode. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
passcodeID - - -string - - - - The UUID of the passcode to be set as active.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 176 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setEmailID(userID, emailID) → {PasscodeState} - - -

- - - - -
- Sets the UUID of the email address. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
emailID - - -string - - - - The UUID of the email address.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 193 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setResendAfter(userID, seconds) → {PasscodeState} - - -

- - - - -
- Sets the number of seconds until a new passcode can be sent. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
seconds - - -number - - - - Number of seconds the passcode is valid for.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 235 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setTTL(userID, seconds) → {PasscodeState} - - -

- - - - -
- Sets the passcode's TTL and stores it to the local storage. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
seconds - - -string - - - - Number of seconds the passcode is valid for.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasscodeState.ts, line 218 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasscodeState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - write() → {State} - - -

- - - - -
- Encodes and writes the data to the local storage. -
- - - - - - - - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 49 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -State - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasswordClient.html b/docs/static/jsdoc/hanko-frontend-sdk/PasswordClient.html deleted file mode 100644 index 7449c2b90..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasswordClient.html +++ /dev/null @@ -1,1205 +0,0 @@ - - - - - - - - PasswordClient - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

PasswordClient

-
- - - - - -
- -
- -

PasswordClient()

- -
A class to handle passwords.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new PasswordClient() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasswordClient.ts, line 14 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -HttpClient - - - - -

- # - - - client - - -

- - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/Client.ts, line 20 - -

- -
- - - - - -
- -
- - - - -PasscodeState - - - - -

- # - - - passcodeState - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasswordClient.ts, line 29 - -

- -
- - - - - -
- -
- - - - -PasswordState - - - - -

- # - - - passwordState - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasswordClient.ts, line 24 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - - getRetryAfter(userID) → {number} - - -

- - - - -
- Returns the number of seconds the rate limiting is active for. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/PasswordClient.ts, line 141 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - login(userID, password) → {Promise.<void>} - - -

- - - - -
- Logs in a user with a password. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
password - - -string - - - - The password.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/PasswordClient.ts, line 119 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -InvalidPasswordError - - -
- - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - - -
- - -
- -TooManyRequestsError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - update(userID, password) → {Promise.<void>} - - -

- - - - -
- Updates a password. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
password - - -string - - - - The new password.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/PasswordClient.ts, line 133 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -UnauthorizedError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasswordConfig.html b/docs/static/jsdoc/hanko-frontend-sdk/PasswordConfig.html index 8cf85148c..937e38e60 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasswordConfig.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/PasswordConfig.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/PasswordState.html b/docs/static/jsdoc/hanko-frontend-sdk/PasswordState.html deleted file mode 100644 index 0dee2f22d..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/PasswordState.html +++ /dev/null @@ -1,1148 +0,0 @@ - - - - - - - - PasswordState - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

PasswordState

-
- - - - - -
- -
- -

PasswordState()

- -
A class that manages the password login state.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new PasswordState() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasswordState.ts, line 11 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -LocalStorage - - - - -

- # - - - ls - - -

- - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 23 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - - getRetryAfter(userID) → {number} - - -

- - - - -
- Gets the number of seconds until when a new password login can be attempted. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasswordState.ts, line 90 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -number - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getUserState(userID) → {LocalStorageUser} - - -

- - - - -
- Gets the state of the specified user. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 25 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -LocalStorageUser - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - read() → {PasswordState} - - -

- - - - -
- Reads the current state. -
- - - - - - - - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasswordState.ts, line 82 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasswordState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setRetryAfter(userID, seconds) → {PasswordState} - - -

- - - - -
- Sets the number of seconds until a new password login can be attempted. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
seconds - - -string - - - - Number of seconds the passcode is valid for.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/PasswordState.ts, line 99 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -PasswordState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - write() → {State} - - -

- - - - -
- Encodes and writes the data to the local storage. -
- - - - - - - - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 49 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -State - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Relay.html b/docs/static/jsdoc/hanko-frontend-sdk/Relay.html index 0c7a0f8e1..5c98c6816 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Relay.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Relay.html @@ -66,7 +66,7 @@ @@ -293,159 +293,6 @@

Methods

-

- # - - - - dispatchAuthFlowCompletedEvent(detail) - - -

- - - - -
- Dispatches a "hanko-auth-flow-completed" event to the document with the specified detail. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
detail - - -AuthFlowCompletedDetail - - - - The event detail.
-
- - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/events/Dispatcher.ts, line 59 - -

- -
- - - - - - - - - - - - - - - - - - - - - - - -
- - -

# diff --git a/docs/static/jsdoc/hanko-frontend-sdk/RelayOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/RelayOptions.html index 20c035807..51df4381e 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/RelayOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/RelayOptions.html @@ -66,7 +66,7 @@

@@ -210,7 +210,7 @@
Properties:

View Source - lib/events/Relay.ts, line 91 + lib/events/Relay.ts, line 87

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/RequestTimeoutError.html b/docs/static/jsdoc/hanko-frontend-sdk/RequestTimeoutError.html index 2db3eb6a3..c042f3682 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/RequestTimeoutError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/RequestTimeoutError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Response.html b/docs/static/jsdoc/hanko-frontend-sdk/Response.html index 971caf8fb..8e2edae25 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Response.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Response.html @@ -66,7 +66,7 @@ @@ -225,7 +225,7 @@
Parameters:

View Source - lib/client/HttpClient.ts, line 39 + lib/client/HttpClient.ts, line 38

@@ -337,7 +337,7 @@

View Source - lib/client/HttpClient.ts, line 53 + lib/client/HttpClient.ts, line 52

@@ -410,7 +410,7 @@

View Source - lib/client/HttpClient.ts, line 58 + lib/client/HttpClient.ts, line 57

@@ -483,7 +483,7 @@

View Source - lib/client/HttpClient.ts, line 63 + lib/client/HttpClient.ts, line 62

@@ -556,7 +556,7 @@

View Source - lib/client/HttpClient.ts, line 68 + lib/client/HttpClient.ts, line 67

@@ -629,7 +629,7 @@

View Source - lib/client/HttpClient.ts, line 73 + lib/client/HttpClient.ts, line 72

@@ -719,7 +719,7 @@

View Source - lib/client/HttpClient.ts, line 295 + lib/client/HttpClient.ts, line 283

@@ -891,7 +891,7 @@

Parameters:

View Source - lib/client/HttpClient.ts, line 304 + lib/client/HttpClient.ts, line 292

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Scheduler.html b/docs/static/jsdoc/hanko-frontend-sdk/Scheduler.html index 3969b8363..30e8639b1 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Scheduler.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Scheduler.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Session.html b/docs/static/jsdoc/hanko-frontend-sdk/Session.html index 6d44b46d2..933a75786 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Session.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Session.html @@ -66,7 +66,7 @@ @@ -347,7 +347,7 @@

View Source - lib/Session.ts, line 95 + lib/Session.ts, line 93

@@ -469,7 +469,7 @@

View Source - lib/Session.ts, line 120 + lib/Session.ts, line 118

@@ -591,7 +591,7 @@

View Source - lib/Session.ts, line 103 + lib/Session.ts, line 101

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/SessionDetail.html b/docs/static/jsdoc/hanko-frontend-sdk/SessionDetail.html index 1a0c90b20..b75c3cd77 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/SessionDetail.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/SessionDetail.html @@ -66,7 +66,7 @@ @@ -185,35 +185,6 @@

Properties:
- - - - userID - - - - - -string - - - - - - - - - - - - - - - - The user associated with the session. - - - @@ -255,7 +226,7 @@
Properties:

View Source - lib/events/CustomEvents.ts, line 54 + lib/events/CustomEvents.ts, line 61

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/SessionOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/SessionOptions.html index fae4cb94a..293e35280 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/SessionOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/SessionOptions.html @@ -66,7 +66,7 @@ @@ -210,7 +210,7 @@
Properties:

View Source - lib/Session.ts, line 78 + lib/Session.ts, line 76

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/SessionState.html b/docs/static/jsdoc/hanko-frontend-sdk/SessionState.html index 51dfc09ae..f2d893285 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/SessionState.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/SessionState.html @@ -66,7 +66,7 @@ @@ -436,7 +436,7 @@

View Source - lib/state/session/SessionState.ts, line 165 + lib/state/session/SessionState.ts, line 134

@@ -556,7 +556,7 @@

View Source - lib/state/session/SessionState.ts, line 137 + lib/state/session/SessionState.ts, line 120

@@ -676,7 +676,7 @@

View Source - lib/state/session/SessionState.ts, line 130 + lib/state/session/SessionState.ts, line 113

@@ -725,126 +725,6 @@

- - -
- - - -

- # - - - - getUserID() → {string} - - -

- - - - -
- Gets the user id. -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/session/SessionState.ts, line 151 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -string - - -
- -
- - -
-
- - - -
@@ -921,7 +801,7 @@

View Source - lib/state/session/SessionState.ts, line 123 + lib/state/session/SessionState.ts, line 106

@@ -1041,7 +921,7 @@

View Source - lib/state/session/SessionState.ts, line 180 + lib/state/session/SessionState.ts, line 149

@@ -1212,7 +1092,7 @@

Parameters:

View Source - lib/state/session/SessionState.ts, line 173 + lib/state/session/SessionState.ts, line 142

@@ -1383,178 +1263,7 @@
Parameters:

View Source - lib/state/session/SessionState.ts, line 145 - -

- - - - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -SessionState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - setUserID(userID) → {SessionState} - - -

- - - - -
- Sets the user id. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The user id
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/session/SessionState.ts, line 159 + lib/state/session/SessionState.ts, line 128

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/SessionStateOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/SessionStateOptions.html index 6f8bdaaee..3f30d5556 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/SessionStateOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/SessionStateOptions.html @@ -66,7 +66,7 @@
@@ -187,7 +187,7 @@

Properties:

View Source - lib/state/session/SessionState.ts, line 100 + lib/state/session/SessionState.ts, line 83

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/SetAuthCookieOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/SetAuthCookieOptions.html deleted file mode 100644 index 6e93239ae..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/SetAuthCookieOptions.html +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - - SetAuthCookieOptions - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Interface

-

SetAuthCookieOptions

-
- - - - - -
- -
- -

SetAuthCookieOptions

- - -
- -
-
- - -
Options for setting the auth cookie.
- - - - - -
Properties:
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
secure - - -boolean - - - - Indicates if the Secure attribute of the cookie should be set.
expires - - -number -| - -Date -| - -undefined - - - - The expiration of the cookie.
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/Cookie.ts, line 50 - -

- -
- - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/State.html b/docs/static/jsdoc/hanko-frontend-sdk/State.html index 2c813751c..56016866c 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/State.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/State.html @@ -66,7 +66,7 @@ @@ -479,7 +479,7 @@
Parameters:

View Source - lib/state/State.ts, line 119 + lib/state/State.ts, line 118

@@ -654,7 +654,7 @@
Parameters:

View Source - lib/state/State.ts, line 110 + lib/state/State.ts, line 109

@@ -774,7 +774,7 @@

View Source - lib/state/State.ts, line 94 + lib/state/State.ts, line 93

@@ -894,7 +894,7 @@

View Source - lib/state/State.ts, line 101 + lib/state/State.ts, line 100

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/TechnicalError.html b/docs/static/jsdoc/hanko-frontend-sdk/TechnicalError.html index 5dc6c3a15..41ab1b64e 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/TechnicalError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/TechnicalError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyClient.html b/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyClient.html index 22723e284..dc4fab339 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyClient.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyError.html b/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyError.html index 5b7114d0f..4295ab47b 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/ThirdPartyError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/Throttle.html b/docs/static/jsdoc/hanko-frontend-sdk/Throttle.html index 581cdd79b..4d91547ba 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/Throttle.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/Throttle.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/ThrottleOptions.html b/docs/static/jsdoc/hanko-frontend-sdk/ThrottleOptions.html index d92114346..090e77c7b 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/ThrottleOptions.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/ThrottleOptions.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/TokenClient.html b/docs/static/jsdoc/hanko-frontend-sdk/TokenClient.html index 4eab75efb..99b8624e6 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/TokenClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/TokenClient.html @@ -66,7 +66,7 @@ @@ -391,7 +391,7 @@

View Source - lib/client/TokenClient.ts, line 52 + lib/client/TokenClient.ts, line 50

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/TokenFinalized.html b/docs/static/jsdoc/hanko-frontend-sdk/TokenFinalized.html index 0023b7cb3..a724ef088 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/TokenFinalized.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/TokenFinalized.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/TooManyRequestsError.html b/docs/static/jsdoc/hanko-frontend-sdk/TooManyRequestsError.html index 92d10fe1d..90384e67a 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/TooManyRequestsError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/TooManyRequestsError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UnauthorizedError.html b/docs/static/jsdoc/hanko-frontend-sdk/UnauthorizedError.html index 837430c5f..318c910d2 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/UnauthorizedError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/UnauthorizedError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/User.html b/docs/static/jsdoc/hanko-frontend-sdk/User.html index f221fec4e..38c1ae9cc 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/User.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/User.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UserClient.html b/docs/static/jsdoc/hanko-frontend-sdk/UserClient.html index 0a7801f16..093ac26c5 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/UserClient.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/UserClient.html @@ -66,7 +66,7 @@ @@ -448,7 +448,7 @@

Parameters:

View Source - lib/client/UserClient.ts, line 172 + lib/client/UserClient.ts, line 168

@@ -624,7 +624,7 @@

View Source - lib/client/UserClient.ts, line 196 + lib/client/UserClient.ts, line 192

@@ -809,7 +809,7 @@

View Source - lib/client/UserClient.ts, line 185 + lib/client/UserClient.ts, line 181

@@ -1045,7 +1045,7 @@

Parameters:

View Source - lib/client/UserClient.ts, line 158 + lib/client/UserClient.ts, line 154

@@ -1221,7 +1221,7 @@

View Source - lib/client/UserClient.ts, line 206 + lib/client/UserClient.ts, line 202

diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UserCreated.html b/docs/static/jsdoc/hanko-frontend-sdk/UserCreated.html index 88ca452fa..cc76b975c 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/UserCreated.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/UserCreated.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UserInfo.html b/docs/static/jsdoc/hanko-frontend-sdk/UserInfo.html index 043b86de2..10b6169b0 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/UserInfo.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/UserInfo.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UserState.html b/docs/static/jsdoc/hanko-frontend-sdk/UserState.html deleted file mode 100644 index 2c20b6a35..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/UserState.html +++ /dev/null @@ -1,831 +0,0 @@ - - - - - - - - UserState - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

UserState

-
- - - - - -
- -
- -

(abstract) UserState(key)

- -
A class to read and write local storage contents.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - abstract - - - - - new UserState(key) - - -

- - - - - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
key - - -string - - - - The local storage key.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 18 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -LocalStorage - - - - -

- # - - - ls - - -

- - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 23 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - - getUserState(userID) → {LocalStorageUser} - - -

- - - - -
- Gets the state of the specified user. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 63 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -LocalStorageUser - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - read() → {State} - - -

- - - - -
- Reads and decodes the locally stored data. -
- - - - - - - - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 30 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -State - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - write() → {State} - - -

- - - - -
- Encodes and writes the data to the local storage. -
- - - - - - - - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 49 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -State - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/UserVerificationError.html b/docs/static/jsdoc/hanko-frontend-sdk/UserVerificationError.html index 7197e1732..d11d79069 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/UserVerificationError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/UserVerificationError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnClient.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnClient.html deleted file mode 100644 index 03458bbe9..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnClient.html +++ /dev/null @@ -1,1920 +0,0 @@ - - - - - - - - WebauthnClient - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

WebauthnClient

-
- - - - - -
- -
- -

WebauthnClient()

- -
A class that handles WebAuthn authentication and registration.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new WebauthnClient() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 16 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -HttpClient - - - - -

- # - - - client - - -

- - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/Client.ts, line 20 - -

- -
- - - - - -
- -
- - - - -PasscodeState - - - - -

- # - - - passcodeState - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 34 - -

- -
- - - - - -
- -
- - - - -WebauthnState - - - - -

- # - - - webauthnState - - -

- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 29 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - async - - - - - deleteCredential(credentialIDopt) → {Promise.<void>} - - -

- - - - -
- Deletes the WebAuthn credential. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
credentialID - - -string - - - - - - <optional>
- - - - - -
The credential's UUID.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 313 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -UnauthorizedError - - -
- - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - listCredentials() → {Promise.<WebauthnCredentials>} - - -

- - - - -
- Returns a list of all WebAuthn credentials assigned to the current user. -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 286 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -UnauthorizedError - - -
- - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<WebauthnCredentials> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - login(userIDopt, useConditionalMediationopt) → {Promise.<void>|WebauthnFinalized} - - -

- - - - -
- Performs a WebAuthn authentication ceremony. When 'userID' is specified, the API provides a list of -allowed credentials and the browser is able to present a list of suitable credentials to the user. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
userID - - -string - - - - - - <optional>
- - - - - -
The user's UUID.
useConditionalMediation - - -boolean - - - - - - <optional>
- - - - - -
Enables autofill assisted login.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 258 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - - - - -
- - - -
- - - - - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - - - -
- - -
- - -WebauthnFinalized - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - register() → {Promise.<void>} - - -

- - - - -
- Performs a WebAuthn registration ceremony. -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 274 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - - - - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -UnauthorizedError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - - -
- - -
- -UserVerificationError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - shouldRegister(user) → {Promise.<boolean>} - - -

- - - - -
- Determines whether a credential registration ceremony should be performed. Returns 'true' when WebAuthn -is supported and the user's credentials do not intersect with the credentials already known on the -current browser/device. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
user - - -User - - - - The user object.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 324 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -Promise.<boolean> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - async - - - - - updateCredential(credentialIDopt, name) → {Promise.<void>} - - -

- - - - -
- Updates the WebAuthn credential. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
credentialID - - -string - - - - - - <optional>
- - - - - -
The credential's UUID.
name - - -string - - - - - - - - - - The new credential name.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
See:
-
- -
- - - - - -

- View Source - - lib/client/WebauthnClient.ts, line 300 - -

- -
- - - - - - - - - - - - - - - - -
-
-
- - -
- - -
- -UnauthorizedError - - -
- - -
- - - -
- - -
- -RequestTimeoutError - - -
- - -
- - - -
- - -
- -TechnicalError - - -
- - -
- - -
-
- - - -
-
-
- - - -
- - -
- - -Promise.<void> - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredential.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredential.html index 19c94561d..54ce65c06 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredential.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredential.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredentials.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredentials.html index e366a3280..5c7d6917e 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredentials.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnCredentials.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnFinalized.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnFinalized.html index dbe1ac4d7..b5bb8973f 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnFinalized.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnFinalized.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnRequestCancelledError.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnRequestCancelledError.html index fabdfca4c..874bb2985 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnRequestCancelledError.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnRequestCancelledError.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnState.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnState.html deleted file mode 100644 index dabf3c9a7..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnState.html +++ /dev/null @@ -1,1345 +0,0 @@ - - - - - - - - WebauthnState - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
- -
-
-
-

Class

-

WebauthnState

-
- - - - - -
- -
- -

WebauthnState()

- -
A class that manages WebAuthn credentials via local storage.
- - -
- -
-
- - -
-
-
-
- Constructor -
- - - - -

- # - - - - new WebauthnState() - - -

- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 10 - -

- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - -

Extends

- - - - - - - - - - - - - - - - - - - - -
-

Members

-
- -
- - - - -LocalStorage - - - - -

- # - - - ls - - -

- - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 23 - -

- -
- - - - - -
- -
-
- - - -
-

Methods

-
- -
- - - -

- # - - - - addCredential(userID, credentialID) → {WebauthnState} - - -

- - - - -
- Adds the credential to the list of known credentials. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
credentialID - - -string - - - - The WebAuthn credential ID.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 111 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -WebauthnState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getCredentials(userID) → {Array.<string>} - - -

- - - - -
- Gets the list of known credentials on the current browser. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 102 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -Array.<string> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - getUserState(userID) → {LocalStorageUser} - - -

- - - - -
- Gets the state of the specified user. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
-
- - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/UserState.ts, line 25 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -LocalStorageUser - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - matchCredentials(userID, match) → {Array.<Credential>} - - -

- - - - -
- Returns the intersection between the specified list of credentials and the known credentials stored in -the local storage. -
- - - - - - - - - - -
Parameters:
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
userID - - -string - - - - The UUID of the user.
match - - -Array.<Credential> - - - - A list of credential IDs to be matched against the local storage.
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 121 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -Array.<Credential> - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - read() → {WebauthnState} - - -

- - - - -
- Reads the current state. -
- - - - - - - - - - - - - - -
- - - - - - - - -
Overrides:
-
- - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/users/WebauthnState.ts, line 94 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -WebauthnState - - -
- -
- - -
-
- - - - -
- -
- - - -

- # - - - - write() → {State} - - -

- - - - -
- Encodes and writes the data to the local storage. -
- - - - - - - - - - - - - - -
- - - - - - -
Inherited From:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- View Source - - lib/state/State.ts, line 49 - -

- -
- - - - - - - - - - - - - - - - - - -
-
-
- - - -
- - -
- - -State - - -
- -
- - -
-
- - - - -
- -
-
- - - - - -
- -
- - - - -
- - - -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnSupport.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnSupport.html index 5b69fed72..3f980dc31 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnSupport.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnSupport.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnTransports.html b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnTransports.html index 9e42f6bd7..8bff5c799 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/WebauthnTransports.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/WebauthnTransports.html @@ -66,7 +66,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/index.html b/docs/static/jsdoc/hanko-frontend-sdk/index.html index 76c55ecc7..c49cc4cb0 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/index.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/index.html @@ -66,7 +66,7 @@ @@ -145,11 +145,7 @@

SDK

Client Classes

    -
  • ConfigClient - A class to fetch configurations.
  • UserClient - A class to manage users.
  • -
  • WebauthnClient - A class to handle WebAuthn-related functionalities.
  • -
  • PasswordClient - A class to manage passwords and password logins.
  • -
  • PasscodeClient - A class to handle passcode logins.
  • ThirdPartyClient - A class to handle social logins.
  • TokenClient - A class that handles the exchange of one time tokens for session JWTs.
@@ -182,12 +178,10 @@

DTO Interfaces

Event Interfaces

  • SessionDetail
  • -
  • AuthFlowCompletedDetail

Event Types

  • CustomEventWithDetail
  • -
  • authFlowCompletedType
  • sessionCreatedType
  • sessionExpiredType
  • userLoggedOutType
  • @@ -230,32 +224,6 @@

    Get the current user / Validate the JWT against the Hanko API

    } }
-

Register a WebAuthn credential

-

There are a number of situations where you may want the user to register a WebAuthn credential. For example, after user -creation, when a user logs in to a new browser/device, or to take advantage of the "caBLE" support and pair a smartphone -with a desktop computer:

-
import { Hanko, UnauthorizedError, WebauthnRequestCancelledError } from "@teamhanko/hanko-frontend-sdk"
-
-const hanko = new Hanko("https://[HANKO_API_URL]")
-
-// By passing the user object (see example above) to `hanko.webauthn.shouldRegister(user)` you get an indication of
-// whether a WebAuthn credential registration should be performed on the current browser. This is useful if the user has
-// logged in using a method other than WebAuthn, and you then want to display a UI that allows the user to register a
-// credential when possibly none exists.
-
-try {
-    // Will cause the browser to present a dialog with various options depending on the WebAuthn implemention.
-    await hanko.webauthn.register()
-
-    // Credential has been registered.
-} catch(e) {
-    if (e instanceof WebauthnRequestCancelledError) {
-        // The WebAuthn API failed. Usually in this case the user cancelled the WebAuthn dialog.
-    } else if (e instanceof UnauthorizedError) {
-        // The user needs to login to perform this action.
-    }
-}
-

Custom Events

You can bind callback functions to different custom events. The callback function will be called when the event happens and an object will be passed in, containing event details. The event binding works as follows:

@@ -268,18 +236,9 @@

Custom Events

The following events are available:

    -
  • "hanko-auth-flow-completed": Will be triggered after a session has been created and the user has completed possible -additional steps (e.g. passkey registration or password recovery) via the <hanko-auth> element.
  • -
-
hanko.onAuthFlowCompleted((authFlowCompletedDetail) => {
-  // Login, registration or recovery has been completed successfully. You can now take control and redirect the
-  // user to protected pages.
-  console.info(`User successfully completed the registration or authorization process (user-id: "${authFlowCompletedDetail.userID}")`);
-})
-
-
    -
  • "hanko-session-created": Will be triggered before the "hanko-auth-flow-completed" happens, as soon as the user is technically logged in. -It will also be triggered when the user logs in via another browser window. The event can be used to obtain the JWT. Please note, that the +
  • "hanko-session-created": Will be triggered after a session has been created and the user has completed possible +additional steps (e.g. passkey registration or password recovery). It will also be triggered when the user logs in via +another browser window. The event can be used to obtain the JWT. Please note, that the JWT is only available, when the Hanko API configuration allows to obtain the JWT. When using Hanko-Cloud the JWT is always present, for self-hosted Hanko-APIs you can restrict the cookie to be readable by the backend only, as long as your backend runs under the same domain as your frontend. To do so, make sure the config parameter "session.enable_auth_token_header" diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_Cookie.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_Cookie.ts.html index 64a27cd51..5e4cb269d 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_Cookie.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_Cookie.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_Dto.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_Dto.ts.html index 5850581cf..8533adb1d 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_Dto.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_Dto.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_Errors.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_Errors.ts.html index 3c41c61ef..1a3c1983c 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_Errors.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_Errors.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_Session.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_Session.ts.html index c29946656..7c32a70d6 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_Session.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_Session.ts.html @@ -68,7 +68,7 @@ @@ -148,12 +148,10 @@

    lib/Session.ts

    public _get(): SessionDetail { this._sessionState.read(); - const userID = this._sessionState.getUserID(); const expirationSeconds = this._sessionState.getExpirationSeconds(); const jwt = this._cookie.getAuthCookie(); return { - userID, expirationSeconds, jwt, }; @@ -174,10 +172,10 @@

    lib/Session.ts

    @private @param {SessionDetail} detail - The session details to validate. - @returns {boolean} true if the session details are valid, false otherwise. + @returns {boolean} true if the session is valid, false otherwise. */ private static validate(detail: SessionDetail): boolean { - return !!(detail.expirationSeconds > 0 && detail.userID?.length); + return detail.expirationSeconds > 0; } } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_Throttle.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_Throttle.ts.html index a93130286..c3cb63e3b 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_Throttle.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_Throttle.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_WebauthnSupport.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_WebauthnSupport.ts.html index ed305924c..c216c09b5 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_WebauthnSupport.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_WebauthnSupport.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_Client.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_Client.ts.html index a07251b18..bb4e91f77 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_Client.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_Client.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ConfigClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ConfigClient.ts.html deleted file mode 100644 index af96b2e07..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ConfigClient.ts.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - lib/client/ConfigClient.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/client/ConfigClient.ts

    -
    - - - - - -
    -
    -
    import { Config } from "../Dto";
    -import { TechnicalError } from "../Errors";
    -import { Client } from "./Client";
    -
    -/**
    - * A class for retrieving configurations from the API.
    - *
    - * @category SDK
    - * @subcategory Clients
    - * @extends {Client}
    - */
    -class ConfigClient extends Client {
    -  /**
    -   * Retrieves the frontend configuration.
    -   * @return {Promise<Config>}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/.well-known/operation/getConfig
    -   */
    -  async get(): Promise<Config> {
    -    const response = await this.client.get("/.well-known/config");
    -
    -    if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    return response.json();
    -  }
    -}
    -
    -export { ConfigClient };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EmailClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EmailClient.ts.html index a8d0a32ba..951537620 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EmailClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EmailClient.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EnterpriseClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EnterpriseClient.ts.html index c62556ef6..c92b236db 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EnterpriseClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_EnterpriseClient.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_HttpClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_HttpClient.ts.html index 5f5b325a3..5280571a4 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_HttpClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_HttpClient.ts.html @@ -68,7 +68,7 @@ @@ -87,7 +87,6 @@

    lib/client/HttpClient.ts

    import { RequestTimeoutError, TechnicalError } from "../Errors";
     import { SessionState } from "../state/session/SessionState";
    -import { PasscodeState } from "../state/users/PasscodeState";
     import { Dispatcher } from "../events/Dispatcher";
     import { Cookie } from "../Cookie";
     
    @@ -229,7 +228,6 @@ 

    lib/client/HttpClient.ts

    timeout: number; api: string; sessionState: SessionState; - passcodeState: PasscodeState; dispatcher: Dispatcher; cookie: Cookie; @@ -238,13 +236,13 @@

    lib/client/HttpClient.ts

    this.api = api; this.timeout = options.timeout; this.sessionState = new SessionState({ ...options }); - this.passcodeState = new PasscodeState(options.cookieName); this.dispatcher = new Dispatcher({ ...options }); this.cookie = new Cookie({ ...options }); } // eslint-disable-next-line require-jsdoc _fetch(path: string, options: RequestInit, xhr = new XMLHttpRequest()) { + const self = this; const url = this.api + path; const timeout = this.timeout; const bearerToken = this.cookie.getAuthCookie(); @@ -261,8 +259,8 @@

    lib/client/HttpClient.ts

    xhr.timeout = timeout; xhr.withCredentials = true; xhr.onload = () => { - const response = new Response(xhr); - resolve(response); + self.processHeaders(xhr); + resolve(new Response(xhr)); }; xhr.onerror = () => { @@ -278,26 +276,24 @@

    lib/client/HttpClient.ts

    } /** - * Processes the response headers on login and extracts the JWT and expiration time. Also, the passcode state will be - * removed, the session state updated und a `hanko-session-created` event will be dispatched. + * Processes the response headers on login and extracts the JWT and expiration time. * - * @param {string} userID - The user ID. - * @param {Response} response - The HTTP response object. + * @param {XMLHttpRequest} xhr - The xhr object. */ - processResponseHeadersOnLogin(userID: string, response: Response) { + processHeaders(xhr: XMLHttpRequest) { let jwt = ""; let expirationSeconds = 0; - response.xhr + xhr .getAllResponseHeaders() .split("\r\n") .forEach((h) => { const header = h.toLowerCase(); if (header.startsWith("x-auth-token")) { - jwt = response.headers.getResponseHeader("X-Auth-Token"); + jwt = xhr.getResponseHeader("X-Auth-Token"); } else if (header.startsWith("x-session-lifetime")) { expirationSeconds = parseInt( - response.headers.getResponseHeader("X-Session-Lifetime"), + xhr.getResponseHeader("X-Session-Lifetime"), 10, ); } @@ -311,19 +307,11 @@

    lib/client/HttpClient.ts

    this.cookie.setAuthCookie(jwt, { secure, expires }); } - this.passcodeState.read().reset(userID).write(); - if (expirationSeconds > 0) { this.sessionState.read(); this.sessionState.setExpirationSeconds(expirationSeconds); - this.sessionState.setUserID(userID); this.sessionState.setAuthFlowCompleted(false); this.sessionState.write(); - this.dispatcher.dispatchSessionCreatedEvent({ - jwt, - userID, - expirationSeconds, - }); } } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasscodeClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasscodeClient.ts.html deleted file mode 100644 index 4059a128b..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasscodeClient.ts.html +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - lib/client/PasscodeClient.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/client/PasscodeClient.ts

    -
    - - - - - -
    -
    -
    import { PasscodeState } from "../state/users/PasscodeState";
    -import { Passcode } from "../Dto";
    -import {
    -  InvalidPasscodeError,
    -  MaxNumOfPasscodeAttemptsReachedError,
    -  PasscodeExpiredError,
    -  TechnicalError,
    -  TooManyRequestsError,
    -} from "../Errors";
    -import { Client } from "./Client";
    -import { HttpClientOptions } from "./HttpClient";
    -
    -/**
    - * A class to handle passcodes.
    - *
    - * @constructor
    - * @category SDK
    - * @subcategory Clients
    - * @extends {Client}
    - */
    -class PasscodeClient extends Client {
    -  state: PasscodeState;
    -
    -  // eslint-disable-next-line require-jsdoc
    -  constructor(api: string, options: HttpClientOptions) {
    -    super(api, options);
    -    /**
    -     *  @public
    -     *  @type {PasscodeState}
    -     */
    -    this.state = new PasscodeState(options.cookieName);
    -  }
    -
    -  /**
    -   * Causes the API to send a new passcode to the user's email address.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string=} emailID - The UUID of the email address. If unspecified, the email will be sent to the primary email address.
    -   * @param {boolean=} force - Indicates the passcode should be sent, even if there is another active passcode.
    -   * @return {Promise<Passcode>}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @throws {TooManyRequestsError}
    -   * @see https://docs.hanko.io/api/public#tag/Passcode/operation/passcodeInit
    -   */
    -  async initialize(
    -    userID: string,
    -    emailID?: string,
    -    force?: boolean
    -  ): Promise<Passcode> {
    -    this.state.read();
    -
    -    const lastPasscodeTTL = this.state.getTTL(userID);
    -    const lastPasscodeID = this.state.getActiveID(userID);
    -    const lastEmailID = this.state.getEmailID(userID);
    -    let retryAfter = this.state.getResendAfter(userID);
    -
    -    if (retryAfter > 0) {
    -      throw new TooManyRequestsError(retryAfter);
    -    }
    -
    -    if (!force && lastPasscodeTTL > 0 && emailID === lastEmailID) {
    -      return {
    -        id: lastPasscodeID,
    -        ttl: lastPasscodeTTL,
    -      };
    -    }
    -
    -    const body: any = { user_id: userID };
    -
    -    if (emailID) {
    -      body.email_id = emailID;
    -    }
    -
    -    const response = await this.client.post(`/passcode/login/initialize`, body);
    -
    -    if (response.status === 429) {
    -      retryAfter = response.parseNumericHeader("Retry-After");
    -      this.state.setResendAfter(userID, retryAfter).write();
    -      throw new TooManyRequestsError(retryAfter);
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    const passcode: Passcode = response.json();
    -
    -    this.state.setActiveID(userID, passcode.id).setTTL(userID, passcode.ttl);
    -
    -    if (emailID) {
    -      this.state.setEmailID(userID, emailID);
    -    }
    -
    -    this.state.write();
    -
    -    return passcode;
    -  }
    -
    -  /**
    -   * Validates the passcode obtained from the email.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} code - The passcode digests.
    -   * @return {Promise<void>}
    -   * @throws {InvalidPasscodeError}
    -   * @throws {MaxNumOfPasscodeAttemptsReachedError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/Passcode/operation/passcodeFinal
    -   */
    -  async finalize(userID: string, code: string): Promise<void> {
    -    const passcodeID = this.state.read().getActiveID(userID);
    -    const ttl = this.state.getTTL(userID);
    -
    -    if (ttl <= 0) {
    -      throw new PasscodeExpiredError();
    -    }
    -
    -    const response = await this.client.post("/passcode/login/finalize", {
    -      id: passcodeID,
    -      code,
    -    });
    -
    -    if (response.status === 401) {
    -      throw new InvalidPasscodeError();
    -    } else if (response.status === 410) {
    -      this.state.reset(userID).write();
    -      throw new MaxNumOfPasscodeAttemptsReachedError();
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    this.client.processResponseHeadersOnLogin(userID, response);
    -
    -    return;
    -  }
    -
    -  /**
    -   * Returns the number of seconds the current passcode is active for.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getTTL(userID: string) {
    -    return this.state.read().getTTL(userID);
    -  }
    -
    -  /**
    -   * Returns the number of seconds the rate limiting is active for.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getResendAfter(userID: string) {
    -    return this.state.read().getResendAfter(userID);
    -  }
    -}
    -
    -export { PasscodeClient };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasswordClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasswordClient.ts.html deleted file mode 100644 index 00d491d09..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_PasswordClient.ts.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - lib/client/PasswordClient.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/client/PasswordClient.ts

    -
    - - - - - -
    -
    -
    import { PasswordState } from "../state/users/PasswordState";
    -import { PasscodeState } from "../state/users/PasscodeState";
    -import {
    -  InvalidPasswordError,
    -  TechnicalError,
    -  TooManyRequestsError,
    -  UnauthorizedError,
    -} from "../Errors";
    -import { Client } from "./Client";
    -import { HttpClientOptions } from "./HttpClient";
    -
    -/**
    - * A class to handle passwords.
    - *
    - * @constructor
    - * @category SDK
    - * @subcategory Clients
    - * @extends {Client}
    - */
    -class PasswordClient extends Client {
    -  passwordState: PasswordState;
    -  passcodeState: PasscodeState;
    -
    -  // eslint-disable-next-line require-jsdoc
    -  constructor(api: string, options: HttpClientOptions) {
    -    super(api, options);
    -    /**
    -     *  @public
    -     *  @type {PasswordState}
    -     */
    -    this.passwordState = new PasswordState(options.cookieName);
    -    /**
    -     *  @public
    -     *  @type {PasscodeState}
    -     */
    -    this.passcodeState = new PasscodeState(options.cookieName);
    -  }
    -
    -  /**
    -   * Logs in a user with a password.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} password - The password.
    -   * @return {Promise<void>}
    -   * @throws {InvalidPasswordError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @throws {TooManyRequestsError}
    -   * @see https://docs.hanko.io/api/public#tag/Password/operation/passwordLogin
    -   */
    -  async login(userID: string, password: string): Promise<void> {
    -    const response = await this.client.post("/password/login", {
    -      user_id: userID,
    -      password,
    -    });
    -
    -    if (response.status === 401) {
    -      throw new InvalidPasswordError();
    -    } else if (response.status === 429) {
    -      const retryAfter = response.parseNumericHeader("Retry-After");
    -      this.passwordState.read().setRetryAfter(userID, retryAfter).write();
    -      throw new TooManyRequestsError(retryAfter);
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    this.client.processResponseHeadersOnLogin(userID, response);
    -    return;
    -  }
    -
    -  /**
    -   * Updates a password.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} password - The new password.
    -   * @return {Promise<void>}
    -   * @throws {RequestTimeoutError}
    -   * @throws {UnauthorizedError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/Password/operation/password
    -   */
    -  async update(userID: string, password: string): Promise<void> {
    -    const response = await this.client.put("/password", {
    -      user_id: userID,
    -      password,
    -    });
    -
    -    if (response.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    return;
    -  }
    -
    -  /**
    -   * Returns the number of seconds the rate limiting is active for.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getRetryAfter(userID: string) {
    -    return this.passwordState.read().getRetryAfter(userID);
    -  }
    -}
    -
    -export { PasswordClient };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ThirdPartyClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ThirdPartyClient.ts.html index 5002256a1..90ed34591 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ThirdPartyClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_ThirdPartyClient.ts.html @@ -68,7 +68,7 @@ @@ -113,14 +113,14 @@

    lib/client/ThirdPartyClient.ts

    if (!provider) { throw new ThirdPartyError( "somethingWentWrong", - new Error("provider missing from request") + new Error("provider missing from request"), ); } if (!redirectTo) { throw new ThirdPartyError( "somethingWentWrong", - new Error("redirectTo missing from request") + new Error("redirectTo missing from request"), ); } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_TokenClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_TokenClient.ts.html index 5e897f6d5..c9c35c246 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_TokenClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_TokenClient.ts.html @@ -68,7 +68,7 @@ @@ -87,7 +87,6 @@

    lib/client/TokenClient.ts

    import { Client } from "./Client";
     import { TechnicalError } from "../Errors";
    -import { TokenFinalized } from "../Dto";
     
     /**
      * Client responsible for exchanging one time tokens for session JWTs.
    @@ -120,8 +119,7 @@ 

    lib/client/TokenClient.ts

    throw new TechnicalError(); } - const tokenResponse: TokenFinalized = response.json(); - this.client.processResponseHeadersOnLogin(tokenResponse.user_id, response); + return response.json(); } }
    diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_UserClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_UserClient.ts.html index 4b9341ea1..013a0d798 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_UserClient.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_UserClient.ts.html @@ -68,7 +68,7 @@ @@ -143,17 +143,14 @@

    lib/client/UserClient.ts

    if (response.status === 409) { throw new ConflictError(); - } if (response.status === 403) { + } + if (response.status === 403) { throw new ForbiddenError(); } else if (!response.ok) { throw new TechnicalError(); } - const createUser: UserCreated = response.json(); - if (createUser && createUser.user_id) { - this.client.processResponseHeadersOnLogin(createUser.user_id, response); - } - return createUser; + return response.json(); } /** diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_WebauthnClient.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_client_WebauthnClient.ts.html deleted file mode 100644 index ac811b41d..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_client_WebauthnClient.ts.html +++ /dev/null @@ -1,417 +0,0 @@ - - - - - - - - - - lib/client/WebauthnClient.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/client/WebauthnClient.ts

    -
    - - - - - -
    -
    -
    import {
    -  create as createWebauthnCredential,
    -  get as getWebauthnCredential,
    -} from "@github/webauthn-json";
    -
    -import { WebauthnSupport } from "../WebauthnSupport";
    -import { Client } from "./Client";
    -import { PasscodeState } from "../state/users/PasscodeState";
    -import { WebauthnState } from "../state/users/WebauthnState";
    -
    -import {
    -  InvalidWebauthnCredentialError,
    -  TechnicalError,
    -  UnauthorizedError,
    -  UserVerificationError,
    -  WebauthnRequestCancelledError,
    -} from "../Errors";
    -
    -import {
    -  Attestation,
    -  User,
    -  WebauthnCredentials,
    -  WebauthnFinalized,
    -} from "../Dto";
    -import { HttpClientOptions } from "./HttpClient";
    -
    -/**
    - * A class that handles WebAuthn authentication and registration.
    - *
    - * @constructor
    - * @category SDK
    - * @subcategory Clients
    - * @extends {Client}
    - */
    -class WebauthnClient extends Client {
    -  webauthnState: WebauthnState;
    -  passcodeState: PasscodeState;
    -  controller: AbortController;
    -  _getCredential = getWebauthnCredential;
    -  _createCredential = createWebauthnCredential;
    -
    -  // eslint-disable-next-line require-jsdoc
    -  constructor(api: string, options: HttpClientOptions) {
    -    super(api, options);
    -    /**
    -     *  @public
    -     *  @type {WebauthnState}
    -     */
    -    this.webauthnState = new WebauthnState(options.cookieName);
    -    /**
    -     *  @public
    -     *  @type {PasscodeState}
    -     */
    -    this.passcodeState = new PasscodeState(options.cookieName);
    -  }
    -
    -  /**
    -   * Performs a WebAuthn authentication ceremony. When 'userID' is specified, the API provides a list of
    -   * allowed credentials and the browser is able to present a list of suitable credentials to the user.
    -   *
    -   * @param {string=} userID - The user's UUID.
    -   * @param {boolean=} useConditionalMediation - Enables autofill assisted login.
    -   * @return {Promise<void>}
    -   * @throws {WebauthnRequestCancelledError}
    -   * @throws {InvalidWebauthnCredentialError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnLoginInit
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnLoginFinal
    -   * @see https://www.w3.org/TR/webauthn-2/#authentication-ceremony
    -   * @return {WebauthnFinalized}
    -   */
    -  async login(
    -    userID?: string,
    -    useConditionalMediation?: boolean
    -  ): Promise<WebauthnFinalized> {
    -    const challengeResponse = await this.client.post(
    -      "/webauthn/login/initialize",
    -      { user_id: userID }
    -    );
    -
    -    if (!challengeResponse.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    const challenge = challengeResponse.json();
    -    challenge.signal = this._createAbortSignal();
    -
    -    if (useConditionalMediation) {
    -      // `CredentialMediationRequirement` doesn't support "conditional" in the current typescript version.
    -      challenge.mediation = "conditional" as CredentialMediationRequirement;
    -    }
    -
    -    let assertion;
    -    try {
    -      assertion = await this._getCredential(challenge);
    -    } catch (e) {
    -      throw new WebauthnRequestCancelledError(e);
    -    }
    -
    -    const assertionResponse = await this.client.post(
    -      "/webauthn/login/finalize",
    -      assertion
    -    );
    -
    -    if (assertionResponse.status === 400 || assertionResponse.status === 401) {
    -      throw new InvalidWebauthnCredentialError();
    -    } else if (!assertionResponse.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    const finalizeResponse: WebauthnFinalized = assertionResponse.json();
    -
    -    this.webauthnState
    -      .read()
    -      .addCredential(finalizeResponse.user_id, finalizeResponse.credential_id)
    -      .write();
    -
    -    this.client.processResponseHeadersOnLogin(
    -      finalizeResponse.user_id,
    -      assertionResponse
    -    );
    -
    -    return finalizeResponse;
    -  }
    -
    -  /**
    -   * Performs a WebAuthn registration ceremony.
    -   *
    -   * @return {Promise<void>}
    -   * @throws {WebauthnRequestCancelledError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {UnauthorizedError}
    -   * @throws {TechnicalError}
    -   * @throws {UserVerificationError}
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnRegInit
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnRegFinal
    -   * @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
    -   */
    -  async register(): Promise<void> {
    -    const challengeResponse = await this.client.post(
    -      "/webauthn/registration/initialize"
    -    );
    -
    -    if (challengeResponse.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    } else if (!challengeResponse.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    const challenge = challengeResponse.json();
    -    challenge.signal = this._createAbortSignal();
    -
    -    let attestation;
    -    try {
    -      attestation = (await this._createCredential(challenge)) as Attestation;
    -    } catch (e) {
    -      throw new WebauthnRequestCancelledError(e);
    -    }
    -
    -    // The generated PublicKeyCredentialWithAttestationJSON object does not align with the API. The list of
    -    // supported transports must be available under a different path.
    -    attestation.transports = attestation.response.transports;
    -
    -    const attestationResponse = await this.client.post(
    -      "/webauthn/registration/finalize",
    -      attestation
    -    );
    -
    -    if (attestationResponse.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    }
    -    if (attestationResponse.status === 422) {
    -      throw new UserVerificationError();
    -    }
    -    if (!attestationResponse.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    const finalizeResponse: WebauthnFinalized = attestationResponse.json();
    -    this.webauthnState
    -      .read()
    -      .addCredential(finalizeResponse.user_id, finalizeResponse.credential_id)
    -      .write();
    -
    -    return;
    -  }
    -
    -  /**
    -   * Returns a list of all WebAuthn credentials assigned to the current user.
    -   *
    -   * @return {Promise<WebauthnCredentials>}
    -   * @throws {UnauthorizedError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/listCredentials
    -   */
    -  async listCredentials(): Promise<WebauthnCredentials> {
    -    const response = await this.client.get("/webauthn/credentials");
    -
    -    if (response.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    return response.json();
    -  }
    -
    -  /**
    -   * Updates the WebAuthn credential.
    -   *
    -   * @param {string=} credentialID - The credential's UUID.
    -   * @param {string} name - The new credential name.
    -   * @return {Promise<void>}
    -   * @throws {UnauthorizedError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/updateCredential
    -   */
    -  async updateCredential(credentialID: string, name: string): Promise<void> {
    -    const response = await this.client.patch(
    -      `/webauthn/credentials/${credentialID}`,
    -      {
    -        name,
    -      }
    -    );
    -
    -    if (response.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    return;
    -  }
    -
    -  /**
    -   * Deletes the WebAuthn credential.
    -   *
    -   * @param {string=} credentialID - The credential's UUID.
    -   * @return {Promise<void>}
    -   * @throws {UnauthorizedError}
    -   * @throws {RequestTimeoutError}
    -   * @throws {TechnicalError}
    -   * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/deleteCredential
    -   */
    -  async deleteCredential(credentialID: string): Promise<void> {
    -    const response = await this.client.delete(
    -      `/webauthn/credentials/${credentialID}`
    -    );
    -
    -    if (response.status === 401) {
    -      this.client.dispatcher.dispatchSessionExpiredEvent();
    -      throw new UnauthorizedError();
    -    } else if (!response.ok) {
    -      throw new TechnicalError();
    -    }
    -
    -    return;
    -  }
    -
    -  /**
    -   * Determines whether a credential registration ceremony should be performed. Returns 'true' when WebAuthn
    -   * is supported and the user's credentials do not intersect with the credentials already known on the
    -   * current browser/device.
    -   *
    -   * @param {User} user - The user object.
    -   * @return {Promise<boolean>}
    -   */
    -  async shouldRegister(user: User): Promise<boolean> {
    -    const supported = WebauthnSupport.supported();
    -
    -    if (!user.webauthn_credentials || !user.webauthn_credentials.length) {
    -      return supported;
    -    }
    -
    -    const matches = this.webauthnState
    -      .read()
    -      .matchCredentials(user.id, user.webauthn_credentials);
    -
    -    return supported && !matches.length;
    -  }
    -
    -  // eslint-disable-next-line require-jsdoc
    -  _createAbortSignal() {
    -    if (this.controller) {
    -      this.controller.abort();
    -    }
    -
    -    this.controller = new AbortController();
    -    return this.controller.signal;
    -  }
    -}
    -
    -export { WebauthnClient };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_CustomEvents.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_CustomEvents.ts.html index 2c35399f6..ea8622401 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_CustomEvents.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_CustomEvents.ts.html @@ -68,7 +68,7 @@ @@ -117,12 +117,18 @@

    lib/events/CustomEvents.ts

    export const userDeletedType: "hanko-user-deleted" = "hanko-user-deleted"; /** - * The type of the `hanko-auth-flow-completed` event. - * @typedef {string} authFlowCompletedType + * The type of the `hanko-user-logged-in` event. + * @typedef {string} userLoggedInType * @memberOf Listener */ -export const authFlowCompletedType: "hanko-auth-flow-completed" = - "hanko-auth-flow-completed"; +export const userLoggedInType: "hanko-user-logged-in" = "hanko-user-logged-in"; + +/** + * The type of the `hanko-user-created` event. + * @typedef {string} userCreatedType + * @memberOf Listener + */ +export const userCreatedType: "hanko-user-created" = "hanko-user-created"; /** * The data passed in the `hanko-session-created` or `hanko-session-resumed` event. @@ -132,24 +138,10 @@

    lib/events/CustomEvents.ts

    * @subcategory Events * @property {string=} jwt - The JSON web token associated with the session. Only present when the Hanko-API allows the JWT to be accessible client-side. * @property {number} expirationSeconds - The number of seconds until the JWT expires. - * @property {string} userID - The user associated with the session. */ export interface SessionDetail { jwt?: string; expirationSeconds: number; - userID: string; -} - -/** - * The data passed in the `hanko-auth-flow-completed` event. - * - * @interface - * @category SDK - * @subcategory Events - * @property {string} userID - The user associated with the removed session. - */ -export interface AuthFlowCompletedDetail { - userID: string; } /** diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Dispatcher.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Dispatcher.ts.html index 9e934bad8..11cdba8a1 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Dispatcher.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Dispatcher.ts.html @@ -68,7 +68,7 @@ @@ -88,11 +88,9 @@

    lib/events/Dispatcher.ts

    import {
       SessionDetail,
       CustomEventWithDetail,
    -  AuthFlowCompletedDetail,
       sessionCreatedType,
       sessionExpiredType,
       userDeletedType,
    -  authFlowCompletedType,
       userLoggedOutType,
     } from "./CustomEvents";
     import { SessionState } from "../state/session/SessionState";
    @@ -164,16 +162,6 @@ 

    lib/events/Dispatcher.ts

    public dispatchUserDeletedEvent() { this.dispatch(userDeletedType, null); } - - /** - * Dispatches a "hanko-auth-flow-completed" event to the document with the specified detail. - * - * @param {AuthFlowCompletedDetail} detail - The event detail. - */ - public dispatchAuthFlowCompletedEvent(detail: AuthFlowCompletedDetail) { - this._sessionState.read().setAuthFlowCompleted(true).write(); - this.dispatch(authFlowCompletedType, detail); - } }
    diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Listener.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Listener.ts.html index c7474343c..9af6d8e2c 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Listener.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Listener.ts.html @@ -68,7 +68,7 @@ @@ -89,11 +89,9 @@

    lib/events/Listener.ts

    import { CustomEventWithDetail, SessionDetail, - AuthFlowCompletedDetail, sessionCreatedType, sessionExpiredType, userDeletedType, - authFlowCompletedType, userLoggedOutType, } from "./CustomEvents"; @@ -171,7 +169,7 @@

    lib/events/Listener.ts

    */ private wrapCallback<T>( callback: CallbackFunc<T>, - throttle: boolean + throttle: boolean, ): WrappedCallback<T> { // The function that will be called when the event is triggered. const wrappedCallback = (event: CustomEventWithDetail<T>) => { @@ -221,7 +219,7 @@

    lib/events/Listener.ts

    private static mapAddEventListenerParams<T>( type: string, { once, callback }: EventListenerParams<T>, - throttle?: boolean + throttle?: boolean, ): EventListenerWithTypeParams<T> { return { type, @@ -243,10 +241,10 @@

    lib/events/Listener.ts

    private addEventListener<T>( type: string, params: EventListenerParams<T>, - throttle?: boolean + throttle?: boolean, ) { return this.addEventListenerWithType( - Listener.mapAddEventListenerParams(type, params, throttle) + Listener.mapAddEventListenerParams(type, params, throttle), ); } @@ -260,7 +258,7 @@

    lib/events/Listener.ts

    */ public onSessionCreated( callback: CallbackFunc<SessionDetail>, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(sessionCreatedType, { callback, once }, true); } @@ -276,7 +274,7 @@

    lib/events/Listener.ts

    */ public onSessionExpired( callback: CallbackFunc<null>, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(sessionExpiredType, { callback, once }, true); } @@ -291,7 +289,7 @@

    lib/events/Listener.ts

    */ public onUserLoggedOut( callback: CallbackFunc<null>, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(userLoggedOutType, { callback, once }); } @@ -305,24 +303,10 @@

    lib/events/Listener.ts

    */ public onUserDeleted( callback: CallbackFunc<null>, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(userDeletedType, { callback, once }); } - - /** - * Adds an event listener for hanko-auth-flow-completed events. Will be triggered after the login or registration flow has been completed. - * - * @param {CallbackFunc<AuthFlowCompletedDetail>} callback - The function to be called when the event is triggered. - * @param {boolean=} once - Whether the event listener should be removed after being called once. - * @returns {CleanupFunc} This function can be called to remove the event listener. - */ - public onAuthFlowCompleted( - callback: CallbackFunc<AuthFlowCompletedDetail>, - once?: boolean - ): CleanupFunc { - return this.addEventListener(authFlowCompletedType, { callback, once }); - } }
    diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Relay.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Relay.ts.html index 346cb8a0f..25685beb2 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Relay.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Relay.ts.html @@ -68,7 +68,7 @@ @@ -136,7 +136,7 @@

    lib/events/Relay.ts

    this._scheduler.scheduleTask( sessionExpiredType, () => this.dispatchSessionExpiredEvent(), - detail.expirationSeconds + detail.expirationSeconds, ); }; @@ -168,11 +168,6 @@

    lib/events/Relay.ts

    return; } - if (this._session.isAuthFlowCompleted()) { - this.dispatchAuthFlowCompletedEvent({ userID: sessionDetail.userID }); - return; - } - this.dispatchSessionCreatedEvent(sessionDetail); }; diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Scheduler.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Scheduler.ts.html index 1fefacbde..58228967b 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Scheduler.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_events_Scheduler.ts.html @@ -68,7 +68,7 @@ diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_Flow.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_Flow.ts.html new file mode 100644 index 000000000..185088816 --- /dev/null +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_Flow.ts.html @@ -0,0 +1,241 @@ + + + + + + + + + + lib/flow-api/Flow.ts + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    +
    +
    + +
    +
    +
    +

    Source

    +

    lib/flow-api/Flow.ts

    +
    + + + + + +
    +
    +
    import { Client } from "../client/Client";
    +import { State, isState } from "./State";
    +import { Action } from "./types/action";
    +import { FetchNextState, FlowPath, Handlers } from "./types/state-handling";
    +
    +type MaybePromise<T> = T | Promise<T>;
    +
    +type ExtendedHandlers = Handlers & { onError?: (e: unknown) => any };
    +type GetInitState = (flow: Flow) => MaybePromise<State<any> | null>;
    +
    +// eslint-disable-next-line require-jsdoc
    +class Flow extends Client {
    +  public async init(
    +    initPath: FlowPath,
    +    handlers: ExtendedHandlers,
    +    // getInitState: GetInitState = () => this.fetchNextState(initPath),
    +  ): Promise<void> {
    +    const fetchNextState: FetchNextState = async (href: string, body?: any) => {
    +      try {
    +        const response = await this.client.post(href, body);
    +        return new State(response.json(), fetchNextState);
    +      } catch (e) {
    +        handlers.onError?.(e);
    +      }
    +    };
    +
    +    const initState = await fetchNextState(initPath);
    +    await this.run(initState, handlers);
    +  }
    +
    +  public async fromString(init: string, handlers: ExtendedHandlers) {
    +    const fetchNextState: FetchNextState = async (href: string, body?: any) => {
    +      try {
    +        const response = await this.client.post(href, body);
    +        return new State(response.json(), fetchNextState);
    +      } catch (e) {
    +        handlers.onError?.(e);
    +      }
    +    };
    +
    +    const initState = new State(JSON.parse(init), fetchNextState);
    +    await this.run(initState, handlers);
    +  }
    +
    +  /**
    +   * Runs a handler for a given state.
    +   *
    +   * If the handler returns an action or a state, this method will run the next
    +   * appropriate handler for that state. (Recursively)
    +   *
    +   * If the handlers passed to `init` do not contain an `onError` handler,
    +   * this method will throw.
    +   *
    +   * @see InvalidStateError
    +   * @see HandlerNotFoundError
    +   *
    +   * @example
    +   * const handlerResult = await run("/login", {
    +   *   // all login handlers are in here, one of which will be called
    +   *   // based on what the /login endpoint returns
    +   * });
    +   */
    +  run = async (
    +    state: State<any>,
    +    handlers: ExtendedHandlers,
    +  ): Promise<unknown> => {
    +    try {
    +      if (!isState(state)) {
    +        throw new InvalidStateError(state);
    +      }
    +
    +      const handler = handlers[state.name];
    +      if (!handler) {
    +        throw new HandlerNotFoundError(state);
    +      }
    +
    +      let maybeNextState = await handler(state);
    +
    +      // handler can return an action, which we'll run (and turn into state)...
    +      if (isAction(maybeNextState)) {
    +        maybeNextState = await (maybeNextState as any).run();
    +      }
    +
    +      // ...or a state, to continue the "run loop"
    +      if (isState(maybeNextState)) {
    +        return this.run(maybeNextState, handlers);
    +      }
    +    } catch (e) {
    +      if (typeof handlers.onError === "function") {
    +        return handlers.onError(e);
    +      }
    +    }
    +  };
    +}
    +
    +export class HandlerNotFoundError extends Error {
    +  constructor(public state: State<any>) {
    +    super(
    +      `No handler found for state: ${
    +        typeof state.name === "string"
    +          ? `"${state.name}"`
    +          : `(${typeof state.name})`
    +      }`,
    +    );
    +  }
    +}
    +
    +export class InvalidStateError extends Error {
    +  constructor(public state: State<any>) {
    +    super(
    +      `Invalid state: ${
    +        typeof state.name === "string"
    +          ? `"${state.name}"`
    +          : `(${typeof state.name})`
    +      }`,
    +    );
    +  }
    +}
    +
    +export function isAction(x: any): x is Action<unknown> {
    +  return typeof x === "object" && x !== null && "href" in x && "inputs" in x;
    +}
    +
    +export { Flow };
    +
    +
    +
    + + + + +
    + + + +
    +
    +
    +
    + + + + + + + diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_State.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_State.ts.html new file mode 100644 index 000000000..b5a05146b --- /dev/null +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_flow-api_State.ts.html @@ -0,0 +1,478 @@ + + + + + + + + + + lib/flow-api/State.ts + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    +
    +
    + +
    +
    +
    +

    Source

    +

    lib/flow-api/State.ts

    +
    + + + + + +
    +
    +
    import {
    +  FetchNextState,
    +  StateName,
    +  Actions,
    +  Payloads,
    +} from "./types/state-handling";
    +import { Error } from "./types/error";
    +import { Action } from "./types/action";
    +import { Input } from "./types/input";
    +
    +type InputValues<TInput extends Record<string, Input<any>>> = {
    +  [K in keyof TInput]?: TInput[K]["value"];
    +};
    +
    +type CreateAction<TAction extends Action<any>> = (
    +  inputs: InputValues<TAction["inputs"]>
    +) => TAction & {
    +  run: () => Promise<State<any>>;
    +  validate: () => TAction;
    +  tryValidate: () => ValidationError | void;
    +};
    +
    +type ActionFunctions = {
    +  [TStateName in keyof Actions]: {
    +    [TActionName in keyof Actions[TStateName]]: Actions[TStateName][TActionName] extends Action<
    +      infer Inputs
    +    >
    +      ? CreateAction<Action<Inputs>>
    +      : never;
    +  };
    +};
    +
    +interface StateResponse<TStateName extends StateName> {
    +  name: StateName;
    +  status: number;
    +  payload?: Payloads[TStateName];
    +  actions?: Actions[TStateName];
    +  csrf_token: string;
    +  error: Error;
    +}
    +
    +// State class represents a state in the flow
    +// eslint-disable-next-line require-jsdoc
    +class State<TStateName extends StateName>
    +  implements Omit<StateResponse<TStateName>, "actions">
    +{
    +  readonly name: StateName;
    +  readonly payload?: Payloads[TStateName];
    +  readonly error: Error;
    +  readonly status: number;
    +  readonly csrf_token: string;
    +
    +  readonly #actionDefinitions: Actions[TStateName];
    +  readonly actions: ActionFunctions[TStateName];
    +
    +  private readonly fetchNextState: FetchNextState;
    +
    +  toJSON() {
    +    return {
    +      name: this.name,
    +      payload: this.payload,
    +      error: this.error,
    +      status: this.status,
    +      csrf_token: this.csrf_token,
    +      actions: this.#actionDefinitions,
    +    };
    +  }
    +
    +  // eslint-disable-next-line require-jsdoc
    +  constructor(
    +    { name, payload, error, status, actions, csrf_token }: StateResponse<TStateName>,
    +    fetchNextState: FetchNextState
    +  ) {
    +    this.name = name;
    +    this.payload = payload;
    +    this.error = error;
    +    this.status = status;
    +    this.csrf_token = csrf_token;
    +    this.#actionDefinitions = actions;
    +
    +    // We're doing something really hacky here, but hear me out
    +    //
    +    // `actions` is an object like this:
    +    //
    +    //     { login_password_recovery: { inputs: { new_password: { min_length: 8, value: "this still needs to be set" } } } }
    +    //
    +    // However, we don't want users to have to mutate the `actions` object manually.
    +    // They WOULD have to do this:
    +    //
    +    //     actions.login_password_recovery.inputs.new_password.value = "password";
    +    //
    +    // Instead, we're going to wrap the `actions` object in a Proxy.
    +    // This Proxy transforms the manual mutation you're seeing above into a function call.
    +    // The following is doing the same thing as the manual mutation above:
    +    //
    +    //     actions.login_password_recovery({ new_password: "password" });
    +    //
    +    // Okay, there's one difference, the function call creates a copy of the action, so it's not mutating the original object.
    +    // The newly created action is returned. It also has a `run` method, which sends the action to the server (fetchNextState)
    +    this.actions = this.#createActionsProxy(actions, csrf_token);
    +
    +    // Do not remove! `this.fetchNextState` has to be set for `this.#runAction` to work
    +    this.fetchNextState = fetchNextState;
    +  }
    +
    +  /**
    +   * We get the `actions` object from the server. That object is essentially a definition of actions that can be performed in the current state.
    +   *
    +   * For example:
    +   *
    +   *     actions = {
    +   *       login_password_recovery: {
    +   *         inputs: {
    +   *           email: { value: undefined, required: true, ... },
    +   *           password: { value: undefined, required: true, min_length: 8, ... }
    +   *         }
    +   *       },
    +   *       create_account: { inputs: ... },
    +   *       some_other_action: { inputs: ... },
    +   *     };
    +   *
    +   * The proxy returned by this method creates "action functions".
    +   *
    +   * Each action function copies the original definition (`{ inputs: ... }`) and modifies that copy with the inputs provided by the user.
    +   *
    +   * In practice, it looks like this:
    +   *
    +   *     actions.login_password_recovery({ new_password: "very-secure-123" });
    +   *     // => { inputs: { password: { value: "very-secure-123", min_length: 8, ... }}}
    +   *
    +   * Additionally, helper methods like `run` (to send the action to the server) and `validate` (to validate the inputs; the `inputs` object also contains validation rules)
    +   */
    +  #createActionsProxy(actions: Actions[TStateName], csrfToken: string) {
    +    const runAction = (action: Action<any>) => this.runAction(action, csrfToken);
    +    const validateAction = (action: Action<any>) => this.validateAction(action);
    +
    +    return new Proxy(actions, {
    +      get(target, prop): CreateAction<Action<unknown>> | undefined {
    +        if (typeof prop === "symbol") return (target as any)[prop];
    +
    +        type Original = Actions[TStateName][keyof Actions[TStateName]];
    +        type Prop = keyof typeof target;
    +
    +        /**
    +         * This is the action defintion.
    +         * Running the function returned by this getter creates a **deep copy**
    +         * with values set by the user.
    +         */
    +        const originalAction = target[
    +          prop as Prop
    +        ] satisfies Original as Action<unknown>;
    +
    +        if (originalAction == null) {
    +          return null;
    +        }
    +
    +        return (newInputs: any) => {
    +          const action = Object.assign(deepCopy(originalAction), {
    +            validate() {
    +              validateAction(action);
    +              return action;
    +            },
    +            tryValidate() {
    +              try {
    +                validateAction(action);
    +              } catch (e) {
    +                if (e instanceof ValidationError) return e;
    +
    +                // We still want to throw non-ValidationErrors since they're unexpected (and indicate a bug on our side)
    +                throw e;
    +              }
    +            },
    +            run() {
    +              return runAction(action);
    +            },
    +          });
    +
    +          // If `actions` is an object that has inputs,
    +          //
    +          // Transform this:
    +          // actions.login_password_recovery({ new_password: "password" });
    +          //                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    +          // Into this:
    +          // action.inputs = { new_password: { min_length: 8, value: "password", ... }}
    +          if (
    +            action !== null &&
    +            typeof action === "object" &&
    +            "inputs" in action
    +          ) {
    +            for (const inputName in newInputs) {
    +              const actionInputs = action.inputs as Record<
    +                string,
    +                Input<unknown>
    +              >;
    +
    +              if (!actionInputs[inputName]) {
    +                actionInputs[inputName] = { name: inputName, type: "" };
    +              }
    +
    +              actionInputs[inputName].value = newInputs[inputName];
    +            }
    +          }
    +
    +          return action;
    +        };
    +      },
    +    }) satisfies Actions[TStateName] as any;
    +  }
    +
    +  runAction(action: Action<any>, csrfToken: string): Promise<State<any>> {
    +    const data: Record<string, any> = {};
    +
    +    // Deal with object-type inputs
    +    // i.e. actions.some_action({ ... })
    +    //                          ^^^^^^^
    +    // Other input types would look like this:
    +    //
    +    // actions.another_action(1234);
    +    // actions.yet_another_action("foo");
    +    //
    +    // Meaning
    +    if (
    +      "inputs" in action &&
    +      typeof action.inputs === "object" &&
    +      action.inputs !== null
    +    ) {
    +      // This looks horrible, but at this point we're sure that `action.inputs` is a Record<string, Input>
    +      // Because there are no object-type inputs that AREN'T a Record<string, Input>
    +      const inputs = action.inputs satisfies object as Record<
    +        string,
    +        Input<unknown>
    +      >;
    +
    +      for (const inputName in action.inputs) {
    +        const input = inputs[inputName];
    +
    +        if (input && "value" in input) {
    +          data[inputName] = input.value;
    +        }
    +      }
    +    }
    +
    +    // (Possibly add more input types here?)
    +
    +    // Use the fetch function to perform the action
    +    return this.fetchNextState(action.href, {
    +      input_data: data,
    +      csrf_token: csrfToken,
    +    });
    +  }
    +
    +  validateAction(action: Action<{ [key: string]: Input<unknown> }>) {
    +    if (!("inputs" in action)) return;
    +
    +    for (const inputName in action.inputs) {
    +      const input = action.inputs[inputName];
    +
    +      function reject<T>(
    +        reason: ValidationReason,
    +        message: string,
    +        wanted?: T,
    +        actual?: T
    +      ) {
    +        throw new ValidationError({
    +          reason,
    +          inputName,
    +          wanted,
    +          actual,
    +          message,
    +        });
    +      }
    +
    +      const value = input.value as any; // TS gets in the way here
    +
    +      // TODO is !input.value right here? this will also reject empty strings, `0`, ... and will never reject an empty array/object
    +      if (input.required && !value) {
    +        reject(ValidationReason.Required, "is required");
    +      }
    +
    +      const hasLengthRequirement =
    +        input.min_length != null || input.max_length != null;
    +
    +      if (hasLengthRequirement) {
    +        if (!("length" in value)) {
    +          reject(
    +            ValidationReason.InvalidInputDefinition,
    +            'has min/max length requirement, but is missing "length" property',
    +            "string",
    +            typeof value
    +          );
    +        }
    +
    +        if (input.min_length != null && value < input.min_length) {
    +          reject(
    +            ValidationReason.MinLength,
    +            `too short (min ${input.min_length})`,
    +            input.min_length,
    +            value.length
    +          );
    +        }
    +
    +        if (input.max_length != null && value > input.max_length) {
    +          reject(
    +            ValidationReason.MaxLength,
    +            `too long (max ${input.max_length})`,
    +            input.max_length,
    +            value.length
    +          );
    +        }
    +      }
    +    }
    +  }
    +}
    +
    +export enum ValidationReason {
    +  InvalidInputDefinition,
    +  MinLength,
    +  MaxLength,
    +  Required,
    +}
    +
    +export class ValidationError<TWanted = undefined> extends Error {
    +  reason: ValidationReason;
    +  inputName: string;
    +  wanted: TWanted;
    +  actual: TWanted;
    +
    +  constructor(opts: {
    +    reason: ValidationReason;
    +    inputName: string;
    +    wanted: TWanted;
    +    actual: TWanted;
    +    message: string;
    +  }) {
    +    super(`"${opts.inputName}" ${opts.message}`);
    +
    +    this.name = "ValidationError";
    +    this.reason = opts.reason;
    +    this.inputName = opts.inputName;
    +    this.wanted = opts.wanted;
    +    this.actual = opts.actual;
    +  }
    +}
    +
    +function deepCopy<T>(obj: T): T {
    +  return JSON.parse(JSON.stringify(obj));
    +}
    +
    +export function isState(x: any): x is State<any> {
    +  return (
    +    typeof x === "object" &&
    +    x !== null &&
    +    "status" in x &&
    +    "error" in x &&
    +    "name" in x &&
    +    Boolean(x.name) &&
    +    Boolean(x.status)
    +  );
    +}
    +
    +export { State };
    +
    +
    +
    + + + + +
    + + + +
    +
    +
    +
    + + + + + + + diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_State.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_State.ts.html index 4d4c558d1..bd1818439 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_State.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_State.ts.html @@ -68,7 +68,7 @@ @@ -85,8 +85,7 @@

    lib/state/State.ts

    -
    import { LocalStorageUsers } from "./users/UserState";
    -import { LocalStorageSession } from "./session/SessionState";
    +            
    import { LocalStorageSession } from "./session/SessionState";
     
     /**
      * @interface
    @@ -95,7 +94,6 @@ 

    lib/state/State.ts

    * @property {LocalStorageUsers=} users - The user states. */ interface LocalStorage { - users?: LocalStorageUsers; session?: LocalStorageSession; } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_session_SessionState.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_session_SessionState.ts.html index 09c8fa889..7240da692 100644 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_session_SessionState.ts.html +++ b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_session_SessionState.ts.html @@ -68,7 +68,7 @@ @@ -106,7 +106,6 @@

    lib/state/session/SessionState.ts

    */ export interface LocalStorageSession { expiry: number; - userID: string; authFlowCompleted: boolean; } @@ -142,7 +141,7 @@

    lib/state/session/SessionState.ts

    * @return {LocalStorageSession} */ getState(): LocalStorageSession { - this.ls.session ||= { expiry: 0, userID: "", authFlowCompleted: false }; + this.ls.session ||= { expiry: 0, authFlowCompleted: false }; return this.ls.session; } @@ -166,24 +165,6 @@

    lib/state/session/SessionState.ts

    return this; } - /** - * Gets the user id. - */ - getUserID(): string { - return this.getState().userID; - } - - /** - * Sets the user id. - * - * @param {string} userID - The user id - * @return {SessionState} - */ - setUserID(userID: string): SessionState { - this.getState().userID = userID; - return this; - } - /** * Gets the authFlowCompleted indicator. */ @@ -211,7 +192,6 @@

    lib/state/session/SessionState.ts

    const session = this.getState(); delete session.expiry; - delete session.userID; return this; } diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasscodeState.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasscodeState.ts.html deleted file mode 100644 index 71fd0f617..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasscodeState.ts.html +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - - - - lib/state/users/PasscodeState.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/state/users/PasscodeState.ts

    -
    - - - - - -
    -
    -
    import { State } from "../State";
    -import { UserState } from "./UserState";
    -
    -/**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {string=} id - The UUID of the active passcode.
    - * @property {number=} ttl - Timestamp until when the passcode is valid in seconds (since January 1, 1970 00:00:00 UTC).
    - * @property {number=} resendAfter - Seconds until a passcode can be resent.
    - * @property {emailID=} emailID - The email address ID.
    - */
    -export interface LocalStoragePasscode {
    -  id?: string;
    -  ttl?: number;
    -  resendAfter?: number;
    -  emailID?: string;
    -}
    -
    -/**
    - * A class that manages passcodes via local storage.
    - *
    - * @extends UserState
    - * @category SDK
    - * @subcategory Internal
    - */
    -class PasscodeState extends UserState {
    -  /**
    -   * Get the passcode state.
    -   *
    -   * @private
    -   * @param {string} userID - The UUID of the user.
    -   * @return {LocalStoragePasscode}
    -   */
    -  private getState(userID: string): LocalStoragePasscode {
    -    return (super.getUserState(userID).passcode ||= {});
    -  }
    -
    -  /**
    -   * Reads the current state.
    -   *
    -   * @public
    -   * @return {PasscodeState}
    -   */
    -  read(): PasscodeState {
    -    super.read();
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the UUID of the active passcode.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {string}
    -   */
    -  getActiveID(userID: string): string {
    -    return this.getState(userID).id;
    -  }
    -
    -  /**
    -   * Sets the UUID of the active passcode.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} passcodeID - The UUID of the passcode to be set as active.
    -   * @return {PasscodeState}
    -   */
    -  setActiveID(userID: string, passcodeID: string): PasscodeState {
    -    this.getState(userID).id = passcodeID;
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the UUID of the email address.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {string}
    -   */
    -  getEmailID(userID: string): string {
    -    return this.getState(userID).emailID;
    -  }
    -
    -  /**
    -   * Sets the UUID of the email address.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} emailID - The UUID of the email address.
    -   * @return {PasscodeState}
    -   */
    -  setEmailID(userID: string, emailID: string): PasscodeState {
    -    this.getState(userID).emailID = emailID;
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Removes the active passcode.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {PasscodeState}
    -   */
    -  reset(userID: string): PasscodeState {
    -    const passcode = this.getState(userID);
    -
    -    delete passcode.id;
    -    delete passcode.ttl;
    -    delete passcode.resendAfter;
    -    delete passcode.emailID;
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the TTL in seconds. When the seconds expire, the code is invalid.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getTTL(userID: string): number {
    -    return State.timeToRemainingSeconds(this.getState(userID).ttl);
    -  }
    -
    -  /**
    -   * Sets the passcode's TTL and stores it to the local storage.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} seconds - Number of seconds the passcode is valid for.
    -   * @return {PasscodeState}
    -   */
    -  setTTL(userID: string, seconds: number): PasscodeState {
    -    this.getState(userID).ttl = State.remainingSecondsToTime(seconds);
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the number of seconds until when the next passcode can be sent.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getResendAfter(userID: string): number {
    -    return State.timeToRemainingSeconds(this.getState(userID).resendAfter);
    -  }
    -
    -  /**
    -   * Sets the number of seconds until a new passcode can be sent.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {number} seconds - Number of seconds the passcode is valid for.
    -   * @return {PasscodeState}
    -   */
    -  setResendAfter(userID: string, seconds: number): PasscodeState {
    -    this.getState(userID).resendAfter = State.remainingSecondsToTime(seconds);
    -
    -    return this;
    -  }
    -}
    -
    -export { PasscodeState };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasswordState.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasswordState.ts.html deleted file mode 100644 index 1eec7b93f..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_PasswordState.ts.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - lib/state/users/PasswordState.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/state/users/PasswordState.ts

    -
    - - - - - -
    -
    -
    import { State } from "../State";
    -import { UserState } from "./UserState";
    -
    -/**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {number=} retryAfter - Timestamp (in seconds since January 1, 1970 00:00:00 UTC) indicating when the next password login can be attempted.
    - */
    -export interface LocalStoragePassword {
    -  retryAfter?: number;
    -}
    -
    -/**
    - * A class that manages the password login state.
    - *
    - * @extends UserState
    - * @category SDK
    - * @subcategory Internal
    - */
    -class PasswordState extends UserState {
    -  /**
    -   * Get the password state.
    -   *
    -   * @private
    -   * @param {string} userID - The UUID of the user.
    -   * @return {LocalStoragePassword}
    -   */
    -  private getState(userID: string): LocalStoragePassword {
    -    return (super.getUserState(userID).password ||= {});
    -  }
    -
    -  /**
    -   * Reads the current state.
    -   *
    -   * @public
    -   * @return {PasswordState}
    -   */
    -  read(): PasswordState {
    -    super.read();
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the number of seconds until when a new password login can be attempted.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {number}
    -   */
    -  getRetryAfter(userID: string): number {
    -    return State.timeToRemainingSeconds(this.getState(userID).retryAfter);
    -  }
    -
    -  /**
    -   * Sets the number of seconds until a new password login can be attempted.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} seconds - Number of seconds the passcode is valid for.
    -   * @return {PasswordState}
    -   */
    -  setRetryAfter(userID: string, seconds: number): PasswordState {
    -    this.getState(userID).retryAfter = State.remainingSecondsToTime(seconds);
    -
    -    return this;
    -  }
    -}
    -
    -export { PasswordState };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_UserState.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_UserState.ts.html deleted file mode 100644 index 58989c440..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_UserState.ts.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - - - lib/state/users/UserState.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/state/users/UserState.ts

    -
    - - - - - -
    -
    -
    /**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {Object.<string, LocalStorageUser>} - A dictionary for mapping users to their states.
    - */
    -import { State } from "../State";
    -
    -import { LocalStorageWebauthn } from "./WebauthnState";
    -import { LocalStoragePasscode } from "./PasscodeState";
    -import { LocalStoragePassword } from "./PasswordState";
    -
    -/**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {LocalStorageWebauthn=} webauthn - Information about WebAuthn credentials.
    - * @property {LocalStoragePasscode=} passcode - Information about the active passcode.
    - * @property {LocalStoragePassword=} password - Information about the password login attempts.
    - */
    -interface LocalStorageUser {
    -  webauthn?: LocalStorageWebauthn;
    -  passcode?: LocalStoragePasscode;
    -  password?: LocalStoragePassword;
    -}
    -
    -/**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {Object.<string, LocalStorageUser>} - A dictionary for mapping users to their states.
    - */
    -export interface LocalStorageUsers {
    -  [userID: string]: LocalStorageUser;
    -}
    -
    -/**
    - * A class to read and write local storage contents.
    - *
    - * @abstract
    - * @extends State
    - * @param {string} key - The local storage key.
    - * @category SDK
    - * @subcategory Internal
    - */
    -abstract class UserState extends State {
    -  /**
    -   * Gets the state of the specified user.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {LocalStorageUser}
    -   */
    -  getUserState(userID: string): LocalStorageUser {
    -    this.ls.users ||= {};
    -
    -    if (!Object.prototype.hasOwnProperty.call(this.ls.users, userID)) {
    -      this.ls.users[userID] = {};
    -    }
    -
    -    return this.ls.users[userID];
    -  }
    -}
    -
    -export { UserState };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_WebauthnState.ts.html b/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_WebauthnState.ts.html deleted file mode 100644 index f6ec12d7d..000000000 --- a/docs/static/jsdoc/hanko-frontend-sdk/lib_state_users_WebauthnState.ts.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - lib/state/users/WebauthnState.ts - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    - -
    -
    -
    -

    Source

    -

    lib/state/users/WebauthnState.ts

    -
    - - - - - -
    -
    -
    import { UserState } from "./UserState";
    -import { Credential } from "../../Dto";
    -
    -/**
    - * @interface
    - * @category SDK
    - * @subcategory Internal
    - * @property {string[]?} credentials - A list of known credential IDs on the current browser.
    - */
    -export interface LocalStorageWebauthn {
    -  credentials?: string[];
    -}
    -
    -/**
    - * A class that manages WebAuthn credentials via local storage.
    - *
    - * @extends UserState
    - * @category SDK
    - * @subcategory Internal
    - */
    -class WebauthnState extends UserState {
    -  /**
    -   * Gets the WebAuthn state.
    -   *
    -   * @private
    -   * @param {string} userID - The UUID of the user.
    -   * @return {LocalStorageWebauthn}
    -   */
    -  private getState(userID: string): LocalStorageWebauthn {
    -    return (super.getUserState(userID).webauthn ||= {});
    -  }
    -
    -  /**
    -   * Reads the current state.
    -   *
    -   * @public
    -   * @return {WebauthnState}
    -   */
    -  read(): WebauthnState {
    -    super.read();
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Gets the list of known credentials on the current browser.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @return {string[]}
    -   */
    -  getCredentials(userID: string): string[] {
    -    return (this.getState(userID).credentials ||= []);
    -  }
    -
    -  /**
    -   * Adds the credential to the list of known credentials.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {string} credentialID - The WebAuthn credential ID.
    -   * @return {WebauthnState}
    -   */
    -  addCredential(userID: string, credentialID: string): WebauthnState {
    -    this.getCredentials(userID).push(credentialID);
    -
    -    return this;
    -  }
    -
    -  /**
    -   * Returns the intersection between the specified list of credentials and the known credentials stored in
    -   * the local storage.
    -   *
    -   * @param {string} userID - The UUID of the user.
    -   * @param {Credential[]} match - A list of credential IDs to be matched against the local storage.
    -   * @return {Credential[]}
    -   */
    -  matchCredentials(userID: string, match: Credential[]): Credential[] {
    -    return this.getCredentials(userID)
    -      .filter((id) => match.find((c) => c.id === id))
    -      .map((id: string) => ({ id } as Credential));
    -  }
    -}
    -
    -export { WebauthnState };
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - - - - - diff --git a/frontend/elements/.nvmrc b/frontend/elements/.nvmrc index 07c7cf304..80a9956e1 100644 --- a/frontend/elements/.nvmrc +++ b/frontend/elements/.nvmrc @@ -1 +1 @@ -v18.14.2 +v20.16.0 diff --git a/frontend/elements/README.md b/frontend/elements/README.md index 41b80e39b..526b35348 100644 --- a/frontend/elements/README.md +++ b/frontend/elements/README.md @@ -74,7 +74,7 @@ pnpm install @teamhanko/hanko-elements To integrate Hanko, you need to import and call the `register()` function from the `hanko-elements` module. Once this is done, you can use the web components in your HTML code. For a functioning page, at least the `` element -should be placed, so the users can sign in, and also, a handler for the "onAuthFlowCompleted" event should be added, to +should be placed, so the users can sign in, and also, a handler for the "onSessionCreated" event should be added, to customize the behaviour after the authentication flow has been completed (e.g. redirect to another page). These steps will be described in the following sections. @@ -149,7 +149,7 @@ of your HTML. A minimal example would look like this: await register("https://hanko.yourdomain.com"); const authComponent = document.getElementById("authComponent"); - authComponent.addEventListener("onAuthFlowCompleted", () => { + authComponent.addEventListener("onSessionCreated", () => { // redirect to a different page }); @@ -219,7 +219,7 @@ handler via the `frontend-sdk` (see next section). ``` @@ -243,25 +243,11 @@ const hanko = new Hanko("https://hanko.yourdomain.com"); It is possible to bind callbacks to different custom events in use of the SDKs event listener functions. The callback function will be called when the event happens and an object will be passed in, containing event details. -##### Auth Flow Completed - -Will be triggered after a session has been created and the user has completed possible -additional steps (e.g. passkey registration or password recovery) via the `` element. - -```js -hanko.onAuthFlowCompleted((authFlowCompletedDetail) => { - // Login, registration or recovery has been completed successfully. You can now take control and redirect the - // user to protected pages. - console.info( - `User successfully completed the registration or authorization process (user-id: "${authFlowCompletedDetail.userID}")` - ); -}); -``` - ##### Session Created -Will be triggered before the "hanko-auth-flow-completed" happens, as soon as the user is technically logged in. It will -also be triggered when the user logs in via another browser window. The event can be used to obtain the JWT. +Will be triggered after a session has been created and the user has completed possible additional steps (e.g. passkey +registration or password recovery). It will also be triggered when the user logs in via another browser window. The +event can be used to obtain the JWT. Please note, that the JWT is only available, when the Hanko-API configuration allows to obtain the JWT. When using Hanko-Cloud the JWT is always present, for self-hosted Hanko-APIs you can restrict the cookie to be readable by the @@ -276,7 +262,7 @@ frontend. hanko.onSessionCreated((sessionDetail) => { // A new JWT has been issued. console.info( - `Session created or updated (user-id: "${sessionDetail.userID}", jwt: ${sessionDetail.jwt})` + `Session created or updated (jwt: ${sessionDetail.jwt})` ); }); ``` diff --git a/frontend/elements/src/contexts/AppProvider.tsx b/frontend/elements/src/contexts/AppProvider.tsx index f355c9929..b3fb04b44 100644 --- a/frontend/elements/src/contexts/AppProvider.tsx +++ b/frontend/elements/src/contexts/AppProvider.tsx @@ -118,7 +118,6 @@ interface Context { componentName: ComponentName; setComponentName: StateUpdater; experimentalFeatures?: ExperimentalFeatures; - emitSuccessEvent: (userID: string) => void; lang: string; hidePasskeyButtonOnLogin: boolean; prefilledEmail?: string; @@ -226,22 +225,6 @@ const AppProvider = ({ ); }; - const emitSuccessEvent = useCallback( - (userID: string) => { - const event = new Event("hankoAuthSuccess", { - bubbles: true, - composed: true, - }); - const fn = setTimeout(() => { - hanko.relay.dispatchAuthFlowCompletedEvent({ userID }); - ref.current.dispatchEvent(event); - }, 500); - - return () => clearTimeout(fn); - }, - [hanko], - ); - const handleError = (e: any) => { setLoadingAction(null); setPage(); @@ -385,12 +368,8 @@ const AppProvider = ({ setPage(); }, success(state) { - hanko.flow.client.processResponseHeadersOnLogin( - state.payload.user.user_id, - hanko.flow.client.response, - ); + hanko.relay.dispatchSessionCreatedEvent(hanko.session.get()); lastActionSucceeded(); - emitSuccessEvent(state.payload.user.user_id); }, profile_init(state) { setPage( @@ -450,7 +429,6 @@ const AppProvider = ({ }, }), [ - emitSuccessEvent, globalOptions.enablePasskeys, hanko, lastActionSucceeded, @@ -506,10 +484,6 @@ const AppProvider = ({ useEffect(() => init(componentName), []); useEffect(() => { - hanko.onAuthFlowCompleted((detail) => { - dispatchEvent("onAuthFlowCompleted", detail); - }); - hanko.onUserDeleted(() => { dispatchEvent("onUserDeleted"); }); @@ -559,7 +533,6 @@ const AppProvider = ({ componentName, setComponentName, experimentalFeatures, - emitSuccessEvent, hidePasskeyButtonOnLogin, page, setPage, diff --git a/frontend/elements/src/example.html b/frontend/elements/src/example.html index 71ddc6b5d..f150fb94a 100644 --- a/frontend/elements/src/example.html +++ b/frontend/elements/src/example.html @@ -228,9 +228,9 @@ // Function to add event listeners function addEventListeners() { hankoEventsEl.addEventListener("onSessionCreated", () => { - // A session has been created through the hanko-auth component, but the authentication flow might not be - // finished yet, so we don't want to hide the hanko-auth component at this time. However, we can show the - // logout button, since the user is logged in. + // The user has completed the authentication flow through the hanko-auth component, so we can display the + // hanko-profile and hide the hanko-auth component. + showAuthComponent(false); // Show profile component setVisibility(logoutButtonEl, true); // Show the logout button // When the dialog was initially opened due to the session expiring in the past, and it has not been closed @@ -240,12 +240,6 @@ } }); - hankoEventsEl.addEventListener("onAuthFlowCompleted", () => { - // The user has completed the authentication flow through the hanko-auth component, so we can display the - // hanko-profile and hide the hanko-auth component. - showAuthComponent(false); // Show profile component - }); - hankoEventsEl.addEventListener("onSessionExpired", () => { // The session has expired, so we can show the dialog to notify the user. Additionally, the logout button // can be hidden. diff --git a/frontend/examples/angular/src/app/login/login.component.html b/frontend/examples/angular/src/app/login/login.component.html index db9f1e3b2..25b0d18b9 100644 --- a/frontend/examples/angular/src/app/login/login.component.html +++ b/frontend/examples/angular/src/app/login/login.component.html @@ -1,4 +1,4 @@
    {{ error?.message }}
    - +
    diff --git a/frontend/examples/nextjs/components/HankoAuth.tsx b/frontend/examples/nextjs/components/HankoAuth.tsx index 918b4f0a7..d2238ef20 100644 --- a/frontend/examples/nextjs/components/HankoAuth.tsx +++ b/frontend/examples/nextjs/components/HankoAuth.tsx @@ -22,7 +22,7 @@ function HankoAuth({ setError }: Props) { register(api).catch(setError); }, [setError]); - useEffect(() => hankoClient?.onAuthFlowCompleted(() => { + useEffect(() => hankoClient?.onSessionCreated(() => { redirectToTodos() }), [hankoClient, redirectToTodos]); diff --git a/frontend/examples/react/src/HankoAuth.tsx b/frontend/examples/react/src/HankoAuth.tsx index c0a905bcd..d1fb191d9 100644 --- a/frontend/examples/react/src/HankoAuth.tsx +++ b/frontend/examples/react/src/HankoAuth.tsx @@ -24,7 +24,7 @@ function HankoAuth() { }, []); useEffect( - () => hankoClient.onAuthFlowCompleted(() => redirectToTodos()), + () => hankoClient.onSessionCreated(() => redirectToTodos()), [hankoClient, redirectToTodos] ); diff --git a/frontend/examples/svelte/src/lib/Login.svelte b/frontend/examples/svelte/src/lib/Login.svelte index 2138c6cf2..bebd8692a 100644 --- a/frontend/examples/svelte/src/lib/Login.svelte +++ b/frontend/examples/svelte/src/lib/Login.svelte @@ -18,6 +18,6 @@ {#if error}
    { error?.message }
    {/if} - + diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs new file mode 100644 index 000000000..6d3e76e8c --- /dev/null +++ b/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs @@ -0,0 +1,13 @@ +// vite.config.ts +import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; +import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +var vite_config_default = defineConfig({ + server: { + host: "0.0.0.0" + }, + plugins: [svelte()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/vue/src/views/LoginView.vue b/frontend/examples/vue/src/views/LoginView.vue index db3004c30..adccfab92 100644 --- a/frontend/examples/vue/src/views/LoginView.vue +++ b/frontend/examples/vue/src/views/LoginView.vue @@ -12,6 +12,6 @@ const redirectToTodos = () => { diff --git a/frontend/frontend-sdk/.nvmrc b/frontend/frontend-sdk/.nvmrc index 07c7cf304..80a9956e1 100644 --- a/frontend/frontend-sdk/.nvmrc +++ b/frontend/frontend-sdk/.nvmrc @@ -1 +1 @@ -v18.14.2 +v20.16.0 diff --git a/frontend/frontend-sdk/README.md b/frontend/frontend-sdk/README.md index 654f641d5..ade588d9f 100644 --- a/frontend/frontend-sdk/README.md +++ b/frontend/frontend-sdk/README.md @@ -62,11 +62,7 @@ To see the latest documentation, please click [here](https://docs.hanko.io/jsdoc ### Client Classes -- `ConfigClient` - A class to fetch configurations. - `UserClient` - A class to manage users. -- `WebauthnClient` - A class to handle WebAuthn-related functionalities. -- `PasswordClient` - A class to manage passwords and password logins. -- `PasscodeClient` - A class to handle passcode logins. - `ThirdPartyClient` - A class to handle social logins. - `TokenClient` - A class that handles the exchange of one time tokens for session JWTs. @@ -99,12 +95,10 @@ To see the latest documentation, please click [here](https://docs.hanko.io/jsdoc ### Event Interfaces - `SessionDetail` -- `AuthFlowCompletedDetail` ### Event Types - `CustomEventWithDetail` -- `authFlowCompletedType` - `sessionCreatedType` - `sessionExpiredType` - `userLoggedOutType` @@ -152,36 +146,6 @@ try { } ``` -### Register a WebAuthn credential - -There are a number of situations where you may want the user to register a WebAuthn credential. For example, after user -creation, when a user logs in to a new browser/device, or to take advantage of the "caBLE" support and pair a smartphone -with a desktop computer: - -```typescript -import { Hanko, UnauthorizedError, WebauthnRequestCancelledError } from "@teamhanko/hanko-frontend-sdk" - -const hanko = new Hanko("https://[HANKO_API_URL]") - -// By passing the user object (see example above) to `hanko.webauthn.shouldRegister(user)` you get an indication of -// whether a WebAuthn credential registration should be performed on the current browser. This is useful if the user has -// logged in using a method other than WebAuthn, and you then want to display a UI that allows the user to register a -// credential when possibly none exists. - -try { - // Will cause the browser to present a dialog with various options depending on the WebAuthn implemention. - await hanko.webauthn.register() - - // Credential has been registered. -} catch(e) { - if (e instanceof WebauthnRequestCancelledError) { - // The WebAuthn API failed. Usually in this case the user cancelled the WebAuthn dialog. - } else if (e instanceof UnauthorizedError) { - // The user needs to login to perform this action. - } -} -``` - ### Custom Events You can bind callback functions to different custom events. The callback function will be called when the event happens @@ -198,19 +162,9 @@ const removeEventListener = hanko.onSessionCreated((eventDetail) => { The following events are available: -- "hanko-auth-flow-completed": Will be triggered after a session has been created and the user has completed possible - additional steps (e.g. passkey registration or password recovery) via the `` element. - -```js -hanko.onAuthFlowCompleted((authFlowCompletedDetail) => { - // Login, registration or recovery has been completed successfully. You can now take control and redirect the - // user to protected pages. - console.info(`User successfully completed the registration or authorization process (user-id: "${authFlowCompletedDetail.userID}")`); -}) -``` - -- "hanko-session-created": Will be triggered before the "hanko-auth-flow-completed" happens, as soon as the user is technically logged in. - It will also be triggered when the user logs in via another browser window. The event can be used to obtain the JWT. Please note, that the +- "hanko-session-created": Will be triggered after a session has been created and the user has completed possible + additional steps (e.g. passkey registration or password recovery). It will also be triggered when the user logs in via + another browser window. The event can be used to obtain the JWT. Please note, that the JWT is only available, when the Hanko API configuration allows to obtain the JWT. When using Hanko-Cloud the JWT is always present, for self-hosted Hanko-APIs you can restrict the cookie to be readable by the backend only, as long as your backend runs under the same domain as your frontend. To do so, make sure the config parameter "session.enable_auth_token_header" diff --git a/frontend/frontend-sdk/src/Hanko.ts b/frontend/frontend-sdk/src/Hanko.ts index 047939534..53f4397e6 100644 --- a/frontend/frontend-sdk/src/Hanko.ts +++ b/frontend/frontend-sdk/src/Hanko.ts @@ -1,9 +1,5 @@ -import { ConfigClient } from "./lib/client/ConfigClient"; import { EnterpriseClient } from "./lib/client/EnterpriseClient"; -import { PasscodeClient } from "./lib/client/PasscodeClient"; -import { PasswordClient } from "./lib/client/PasswordClient"; import { UserClient } from "./lib/client/UserClient"; -import { WebauthnClient } from "./lib/client/WebauthnClient"; import { EmailClient } from "./lib/client/EmailClient"; import { ThirdPartyClient } from "./lib/client/ThirdPartyClient"; import { TokenClient } from "./lib/client/TokenClient"; @@ -40,11 +36,7 @@ export interface HankoOptions { */ class Hanko extends Listener { api: string; - config: ConfigClient; user: UserClient; - webauthn: WebauthnClient; - password: PasswordClient; - passcode: PasscodeClient; email: EmailClient; thirdParty: ThirdPartyClient; enterprise: EnterpriseClient; @@ -78,31 +70,11 @@ class Hanko extends Listener { } this.api = api; - /** - * @public - * @type {ConfigClient} - */ - this.config = new ConfigClient(api, opts); /** * @public * @type {UserClient} */ this.user = new UserClient(api, opts); - /** - * @public - * @type {WebauthnClient} - */ - this.webauthn = new WebauthnClient(api, opts); - /** - * @public - * @type {PasswordClient} - */ - this.password = new PasswordClient(api, opts); - /** - * @public - * @type {PasscodeClient} - */ - this.passcode = new PasscodeClient(api, opts); /** * @public * @type {EmailClient} diff --git a/frontend/frontend-sdk/src/declarations.d.ts b/frontend/frontend-sdk/src/declarations.d.ts index 0372e0b5e..397df1c77 100644 --- a/frontend/frontend-sdk/src/declarations.d.ts +++ b/frontend/frontend-sdk/src/declarations.d.ts @@ -1,12 +1,10 @@ import { CustomEventWithDetail, SessionDetail, - AuthFlowCompletedDetail, sessionCreatedType, sessionExpiredType, userLoggedOutType, userDeletedType, - authFlowCompletedType, } from "./lib/events/CustomEvents"; declare global { @@ -16,7 +14,6 @@ declare global { [sessionExpiredType]: CustomEventWithDetail; [userLoggedOutType]: CustomEventWithDetail; [userDeletedType]: CustomEventWithDetail; - [authFlowCompletedType]: CustomEventWithDetail; } } diff --git a/frontend/frontend-sdk/src/index.ts b/frontend/frontend-sdk/src/index.ts index c88d8c94d..0d5b69900 100644 --- a/frontend/frontend-sdk/src/index.ts +++ b/frontend/frontend-sdk/src/index.ts @@ -6,22 +6,14 @@ export { Hanko }; // Clients -import { ConfigClient } from "./lib/client/ConfigClient"; -import { PasscodeClient } from "./lib/client/PasscodeClient"; -import { PasswordClient } from "./lib/client/PasswordClient"; import { UserClient } from "./lib/client/UserClient"; -import { WebauthnClient } from "./lib/client/WebauthnClient"; import { EmailClient } from "./lib/client/EmailClient"; import { ThirdPartyClient } from "./lib/client/ThirdPartyClient"; import { TokenClient } from "./lib/client/TokenClient"; import { EnterpriseClient } from "./lib/client/EnterpriseClient"; export { - ConfigClient, UserClient, - WebauthnClient, - PasswordClient, - PasscodeClient, EmailClient, ThirdPartyClient, TokenClient, @@ -129,18 +121,15 @@ export { import { CustomEventWithDetail, SessionDetail, - AuthFlowCompletedDetail, - authFlowCompletedType, sessionCreatedType, sessionExpiredType, userLoggedOutType, userDeletedType, } from "./lib/events/CustomEvents"; -export type { SessionDetail, AuthFlowCompletedDetail }; +export type { SessionDetail }; export { - authFlowCompletedType, sessionCreatedType, sessionExpiredType, userLoggedOutType, diff --git a/frontend/frontend-sdk/src/lib/Session.ts b/frontend/frontend-sdk/src/lib/Session.ts index c5f653c8e..ba8cd2f1f 100644 --- a/frontend/frontend-sdk/src/lib/Session.ts +++ b/frontend/frontend-sdk/src/lib/Session.ts @@ -61,12 +61,10 @@ export class Session { public _get(): SessionDetail { this._sessionState.read(); - const userID = this._sessionState.getUserID(); const expirationSeconds = this._sessionState.getExpirationSeconds(); const jwt = this._cookie.getAuthCookie(); return { - userID, expirationSeconds, jwt, }; @@ -87,9 +85,9 @@ export class Session { @private @param {SessionDetail} detail - The session details to validate. - @returns {boolean} true if the session details are valid, false otherwise. + @returns {boolean} true if the session is valid, false otherwise. */ private static validate(detail: SessionDetail): boolean { - return !!(detail.expirationSeconds > 0 && detail.userID?.length); + return detail.expirationSeconds > 0; } } diff --git a/frontend/frontend-sdk/src/lib/client/ConfigClient.ts b/frontend/frontend-sdk/src/lib/client/ConfigClient.ts deleted file mode 100644 index 206dd3484..000000000 --- a/frontend/frontend-sdk/src/lib/client/ConfigClient.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Config } from "../Dto"; -import { TechnicalError } from "../Errors"; -import { Client } from "./Client"; - -/** - * A class for retrieving configurations from the API. - * - * @category SDK - * @subcategory Clients - * @extends {Client} - */ -class ConfigClient extends Client { - /** - * Retrieves the frontend configuration. - * @return {Promise} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/.well-known/operation/getConfig - */ - async get(): Promise { - const response = await this.client.get("/.well-known/config"); - - if (!response.ok) { - throw new TechnicalError(); - } - - return response.json(); - } -} - -export { ConfigClient }; diff --git a/frontend/frontend-sdk/src/lib/client/HttpClient.ts b/frontend/frontend-sdk/src/lib/client/HttpClient.ts index a4a220493..6058a6e32 100644 --- a/frontend/frontend-sdk/src/lib/client/HttpClient.ts +++ b/frontend/frontend-sdk/src/lib/client/HttpClient.ts @@ -1,6 +1,5 @@ import { RequestTimeoutError, TechnicalError } from "../Errors"; import { SessionState } from "../state/session/SessionState"; -import { PasscodeState } from "../state/users/PasscodeState"; import { Dispatcher } from "../events/Dispatcher"; import { Cookie } from "../Cookie"; @@ -142,17 +141,14 @@ class HttpClient { timeout: number; api: string; sessionState: SessionState; - passcodeState: PasscodeState; dispatcher: Dispatcher; cookie: Cookie; - response: Response; // eslint-disable-next-line require-jsdoc constructor(api: string, options: HttpClientOptions) { this.api = api; this.timeout = options.timeout; this.sessionState = new SessionState({ ...options }); - this.passcodeState = new PasscodeState(options.cookieName); this.dispatcher = new Dispatcher({ ...options }); this.cookie = new Cookie({ ...options }); } @@ -176,7 +172,8 @@ class HttpClient { xhr.timeout = timeout; xhr.withCredentials = true; xhr.onload = () => { - resolve((self.response = new Response(xhr))); + self.processHeaders(xhr); + resolve(new Response(xhr)); }; xhr.onerror = () => { @@ -192,26 +189,24 @@ class HttpClient { } /** - * Processes the response headers on login and extracts the JWT and expiration time. Also, the passcode state will be - * removed, the session state updated und a `hanko-session-created` event will be dispatched. + * Processes the response headers on login and extracts the JWT and expiration time. * - * @param {string} userID - The user ID. - * @param {Response} response - The HTTP response object. + * @param {XMLHttpRequest} xhr - The xhr object. */ - processResponseHeadersOnLogin(userID: string, response: Response) { + processHeaders(xhr: XMLHttpRequest) { let jwt = ""; let expirationSeconds = 0; - response.xhr + xhr .getAllResponseHeaders() .split("\r\n") .forEach((h) => { const header = h.toLowerCase(); if (header.startsWith("x-auth-token")) { - jwt = response.headers.getResponseHeader("X-Auth-Token"); + jwt = xhr.getResponseHeader("X-Auth-Token"); } else if (header.startsWith("x-session-lifetime")) { expirationSeconds = parseInt( - response.headers.getResponseHeader("X-Session-Lifetime"), + xhr.getResponseHeader("X-Session-Lifetime"), 10, ); } @@ -225,19 +220,11 @@ class HttpClient { this.cookie.setAuthCookie(jwt, { secure, expires }); } - this.passcodeState.read().reset(userID).write(); - if (expirationSeconds > 0) { this.sessionState.read(); this.sessionState.setExpirationSeconds(expirationSeconds); - this.sessionState.setUserID(userID); this.sessionState.setAuthFlowCompleted(false); this.sessionState.write(); - this.dispatcher.dispatchSessionCreatedEvent({ - jwt, - userID, - expirationSeconds, - }); } } diff --git a/frontend/frontend-sdk/src/lib/client/PasscodeClient.ts b/frontend/frontend-sdk/src/lib/client/PasscodeClient.ts deleted file mode 100644 index 4c114e91b..000000000 --- a/frontend/frontend-sdk/src/lib/client/PasscodeClient.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { PasscodeState } from "../state/users/PasscodeState"; -import { Passcode } from "../Dto"; -import { - InvalidPasscodeError, - MaxNumOfPasscodeAttemptsReachedError, - PasscodeExpiredError, - TechnicalError, - TooManyRequestsError, -} from "../Errors"; -import { Client } from "./Client"; -import { HttpClientOptions } from "./HttpClient"; - -/** - * A class to handle passcodes. - * - * @constructor - * @category SDK - * @subcategory Clients - * @extends {Client} - */ -class PasscodeClient extends Client { - state: PasscodeState; - - // eslint-disable-next-line require-jsdoc - constructor(api: string, options: HttpClientOptions) { - super(api, options); - /** - * @public - * @type {PasscodeState} - */ - this.state = new PasscodeState(options.cookieName); - } - - /** - * Causes the API to send a new passcode to the user's email address. - * - * @param {string} userID - The UUID of the user. - * @param {string=} emailID - The UUID of the email address. If unspecified, the email will be sent to the primary email address. - * @param {boolean=} force - Indicates the passcode should be sent, even if there is another active passcode. - * @return {Promise} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @throws {TooManyRequestsError} - * @see https://docs.hanko.io/api/public#tag/Passcode/operation/passcodeInit - */ - async initialize( - userID: string, - emailID?: string, - force?: boolean - ): Promise { - this.state.read(); - - const lastPasscodeTTL = this.state.getTTL(userID); - const lastPasscodeID = this.state.getActiveID(userID); - const lastEmailID = this.state.getEmailID(userID); - let retryAfter = this.state.getResendAfter(userID); - - if (retryAfter > 0) { - throw new TooManyRequestsError(retryAfter); - } - - if (!force && lastPasscodeTTL > 0 && emailID === lastEmailID) { - return { - id: lastPasscodeID, - ttl: lastPasscodeTTL, - }; - } - - const body: any = { user_id: userID }; - - if (emailID) { - body.email_id = emailID; - } - - const response = await this.client.post(`/passcode/login/initialize`, body); - - if (response.status === 429) { - retryAfter = response.parseNumericHeader("Retry-After"); - this.state.setResendAfter(userID, retryAfter).write(); - throw new TooManyRequestsError(retryAfter); - } else if (!response.ok) { - throw new TechnicalError(); - } - - const passcode: Passcode = response.json(); - - this.state.setActiveID(userID, passcode.id).setTTL(userID, passcode.ttl); - - if (emailID) { - this.state.setEmailID(userID, emailID); - } - - this.state.write(); - - return passcode; - } - - /** - * Validates the passcode obtained from the email. - * - * @param {string} userID - The UUID of the user. - * @param {string} code - The passcode digests. - * @return {Promise} - * @throws {InvalidPasscodeError} - * @throws {MaxNumOfPasscodeAttemptsReachedError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/Passcode/operation/passcodeFinal - */ - async finalize(userID: string, code: string): Promise { - const passcodeID = this.state.read().getActiveID(userID); - const ttl = this.state.getTTL(userID); - - if (ttl <= 0) { - throw new PasscodeExpiredError(); - } - - const response = await this.client.post("/passcode/login/finalize", { - id: passcodeID, - code, - }); - - if (response.status === 401) { - throw new InvalidPasscodeError(); - } else if (response.status === 410) { - this.state.reset(userID).write(); - throw new MaxNumOfPasscodeAttemptsReachedError(); - } else if (!response.ok) { - throw new TechnicalError(); - } - - this.client.processResponseHeadersOnLogin(userID, response); - - return; - } - - /** - * Returns the number of seconds the current passcode is active for. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getTTL(userID: string) { - return this.state.read().getTTL(userID); - } - - /** - * Returns the number of seconds the rate limiting is active for. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getResendAfter(userID: string) { - return this.state.read().getResendAfter(userID); - } -} - -export { PasscodeClient }; diff --git a/frontend/frontend-sdk/src/lib/client/PasswordClient.ts b/frontend/frontend-sdk/src/lib/client/PasswordClient.ts deleted file mode 100644 index 7f88cf823..000000000 --- a/frontend/frontend-sdk/src/lib/client/PasswordClient.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { PasswordState } from "../state/users/PasswordState"; -import { PasscodeState } from "../state/users/PasscodeState"; -import { - InvalidPasswordError, - TechnicalError, - TooManyRequestsError, - UnauthorizedError, -} from "../Errors"; -import { Client } from "./Client"; -import { HttpClientOptions } from "./HttpClient"; - -/** - * A class to handle passwords. - * - * @constructor - * @category SDK - * @subcategory Clients - * @extends {Client} - */ -class PasswordClient extends Client { - passwordState: PasswordState; - passcodeState: PasscodeState; - - // eslint-disable-next-line require-jsdoc - constructor(api: string, options: HttpClientOptions) { - super(api, options); - /** - * @public - * @type {PasswordState} - */ - this.passwordState = new PasswordState(options.cookieName); - /** - * @public - * @type {PasscodeState} - */ - this.passcodeState = new PasscodeState(options.cookieName); - } - - /** - * Logs in a user with a password. - * - * @param {string} userID - The UUID of the user. - * @param {string} password - The password. - * @return {Promise} - * @throws {InvalidPasswordError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @throws {TooManyRequestsError} - * @see https://docs.hanko.io/api/public#tag/Password/operation/passwordLogin - */ - async login(userID: string, password: string): Promise { - const response = await this.client.post("/password/login", { - user_id: userID, - password, - }); - - if (response.status === 401) { - throw new InvalidPasswordError(); - } else if (response.status === 429) { - const retryAfter = response.parseNumericHeader("Retry-After"); - this.passwordState.read().setRetryAfter(userID, retryAfter).write(); - throw new TooManyRequestsError(retryAfter); - } else if (!response.ok) { - throw new TechnicalError(); - } - - this.client.processResponseHeadersOnLogin(userID, response); - return; - } - - /** - * Updates a password. - * - * @param {string} userID - The UUID of the user. - * @param {string} password - The new password. - * @return {Promise} - * @throws {RequestTimeoutError} - * @throws {UnauthorizedError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/Password/operation/password - */ - async update(userID: string, password: string): Promise { - const response = await this.client.put("/password", { - user_id: userID, - password, - }); - - if (response.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } else if (!response.ok) { - throw new TechnicalError(); - } - - return; - } - - /** - * Returns the number of seconds the rate limiting is active for. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getRetryAfter(userID: string) { - return this.passwordState.read().getRetryAfter(userID); - } -} - -export { PasswordClient }; diff --git a/frontend/frontend-sdk/src/lib/client/ThirdPartyClient.ts b/frontend/frontend-sdk/src/lib/client/ThirdPartyClient.ts index 9b5718d19..6f93624b5 100644 --- a/frontend/frontend-sdk/src/lib/client/ThirdPartyClient.ts +++ b/frontend/frontend-sdk/src/lib/client/ThirdPartyClient.ts @@ -26,14 +26,14 @@ export class ThirdPartyClient extends Client { if (!provider) { throw new ThirdPartyError( "somethingWentWrong", - new Error("provider missing from request") + new Error("provider missing from request"), ); } if (!redirectTo) { throw new ThirdPartyError( "somethingWentWrong", - new Error("redirectTo missing from request") + new Error("redirectTo missing from request"), ); } diff --git a/frontend/frontend-sdk/src/lib/client/TokenClient.ts b/frontend/frontend-sdk/src/lib/client/TokenClient.ts index fa2cb921a..60a605cfb 100644 --- a/frontend/frontend-sdk/src/lib/client/TokenClient.ts +++ b/frontend/frontend-sdk/src/lib/client/TokenClient.ts @@ -1,6 +1,5 @@ import { Client } from "./Client"; import { TechnicalError } from "../Errors"; -import { TokenFinalized } from "../Dto"; /** * Client responsible for exchanging one time tokens for session JWTs. @@ -33,7 +32,6 @@ export class TokenClient extends Client { throw new TechnicalError(); } - const tokenResponse: TokenFinalized = response.json(); - this.client.processResponseHeadersOnLogin(tokenResponse.user_id, response); + return response.json(); } } diff --git a/frontend/frontend-sdk/src/lib/client/UserClient.ts b/frontend/frontend-sdk/src/lib/client/UserClient.ts index dddecca2a..c1dca50fa 100644 --- a/frontend/frontend-sdk/src/lib/client/UserClient.ts +++ b/frontend/frontend-sdk/src/lib/client/UserClient.ts @@ -56,17 +56,14 @@ class UserClient extends Client { if (response.status === 409) { throw new ConflictError(); - } if (response.status === 403) { + } + if (response.status === 403) { throw new ForbiddenError(); } else if (!response.ok) { throw new TechnicalError(); } - const createUser: UserCreated = response.json(); - if (createUser && createUser.user_id) { - this.client.processResponseHeadersOnLogin(createUser.user_id, response); - } - return createUser; + return response.json(); } /** diff --git a/frontend/frontend-sdk/src/lib/client/WebauthnClient.ts b/frontend/frontend-sdk/src/lib/client/WebauthnClient.ts deleted file mode 100644 index 0c517ce93..000000000 --- a/frontend/frontend-sdk/src/lib/client/WebauthnClient.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { - create as createWebauthnCredential, - get as getWebauthnCredential, -} from "@github/webauthn-json"; - -import { WebauthnSupport } from "../WebauthnSupport"; -import { Client } from "./Client"; -import { PasscodeState } from "../state/users/PasscodeState"; -import { WebauthnState } from "../state/users/WebauthnState"; - -import { - InvalidWebauthnCredentialError, - TechnicalError, - UnauthorizedError, - UserVerificationError, - WebauthnRequestCancelledError, -} from "../Errors"; - -import { - Attestation, - User, - WebauthnCredentials, - WebauthnFinalized, -} from "../Dto"; -import { HttpClientOptions } from "./HttpClient"; - -/** - * A class that handles WebAuthn authentication and registration. - * - * @constructor - * @category SDK - * @subcategory Clients - * @extends {Client} - */ -class WebauthnClient extends Client { - webauthnState: WebauthnState; - passcodeState: PasscodeState; - controller: AbortController; - _getCredential = getWebauthnCredential; - _createCredential = createWebauthnCredential; - - // eslint-disable-next-line require-jsdoc - constructor(api: string, options: HttpClientOptions) { - super(api, options); - /** - * @public - * @type {WebauthnState} - */ - this.webauthnState = new WebauthnState(options.cookieName); - /** - * @public - * @type {PasscodeState} - */ - this.passcodeState = new PasscodeState(options.cookieName); - } - - /** - * Performs a WebAuthn authentication ceremony. When 'userID' is specified, the API provides a list of - * allowed credentials and the browser is able to present a list of suitable credentials to the user. - * - * @param {string=} userID - The user's UUID. - * @param {boolean=} useConditionalMediation - Enables autofill assisted login. - * @return {Promise} - * @throws {WebauthnRequestCancelledError} - * @throws {InvalidWebauthnCredentialError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnLoginInit - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnLoginFinal - * @see https://www.w3.org/TR/webauthn-2/#authentication-ceremony - * @return {WebauthnFinalized} - */ - async login( - userID?: string, - useConditionalMediation?: boolean - ): Promise { - const challengeResponse = await this.client.post( - "/webauthn/login/initialize", - { user_id: userID } - ); - - if (!challengeResponse.ok) { - throw new TechnicalError(); - } - - const challenge = challengeResponse.json(); - challenge.signal = this._createAbortSignal(); - - if (useConditionalMediation) { - // `CredentialMediationRequirement` doesn't support "conditional" in the current typescript version. - challenge.mediation = "conditional" as CredentialMediationRequirement; - } - - let assertion; - try { - assertion = await this._getCredential(challenge); - } catch (e) { - throw new WebauthnRequestCancelledError(e); - } - - const assertionResponse = await this.client.post( - "/webauthn/login/finalize", - assertion - ); - - if (assertionResponse.status === 400 || assertionResponse.status === 401) { - throw new InvalidWebauthnCredentialError(); - } else if (!assertionResponse.ok) { - throw new TechnicalError(); - } - - const finalizeResponse: WebauthnFinalized = assertionResponse.json(); - - this.webauthnState - .read() - .addCredential(finalizeResponse.user_id, finalizeResponse.credential_id) - .write(); - - this.client.processResponseHeadersOnLogin( - finalizeResponse.user_id, - assertionResponse - ); - - return finalizeResponse; - } - - /** - * Performs a WebAuthn registration ceremony. - * - * @return {Promise} - * @throws {WebauthnRequestCancelledError} - * @throws {RequestTimeoutError} - * @throws {UnauthorizedError} - * @throws {TechnicalError} - * @throws {UserVerificationError} - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnRegInit - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/webauthnRegFinal - * @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential - */ - async register(): Promise { - const challengeResponse = await this.client.post( - "/webauthn/registration/initialize" - ); - - if (challengeResponse.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } else if (!challengeResponse.ok) { - throw new TechnicalError(); - } - - const challenge = challengeResponse.json(); - challenge.signal = this._createAbortSignal(); - - let attestation; - try { - attestation = (await this._createCredential(challenge)) as Attestation; - } catch (e) { - throw new WebauthnRequestCancelledError(e); - } - - // The generated PublicKeyCredentialWithAttestationJSON object does not align with the API. The list of - // supported transports must be available under a different path. - attestation.transports = attestation.response.transports; - - const attestationResponse = await this.client.post( - "/webauthn/registration/finalize", - attestation - ); - - if (attestationResponse.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } - if (attestationResponse.status === 422) { - throw new UserVerificationError(); - } - if (!attestationResponse.ok) { - throw new TechnicalError(); - } - - const finalizeResponse: WebauthnFinalized = attestationResponse.json(); - this.webauthnState - .read() - .addCredential(finalizeResponse.user_id, finalizeResponse.credential_id) - .write(); - - return; - } - - /** - * Returns a list of all WebAuthn credentials assigned to the current user. - * - * @return {Promise} - * @throws {UnauthorizedError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/listCredentials - */ - async listCredentials(): Promise { - const response = await this.client.get("/webauthn/credentials"); - - if (response.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } else if (!response.ok) { - throw new TechnicalError(); - } - - return response.json(); - } - - /** - * Updates the WebAuthn credential. - * - * @param {string=} credentialID - The credential's UUID. - * @param {string} name - The new credential name. - * @return {Promise} - * @throws {UnauthorizedError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/updateCredential - */ - async updateCredential(credentialID: string, name: string): Promise { - const response = await this.client.patch( - `/webauthn/credentials/${credentialID}`, - { - name, - } - ); - - if (response.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } else if (!response.ok) { - throw new TechnicalError(); - } - - return; - } - - /** - * Deletes the WebAuthn credential. - * - * @param {string=} credentialID - The credential's UUID. - * @return {Promise} - * @throws {UnauthorizedError} - * @throws {RequestTimeoutError} - * @throws {TechnicalError} - * @see https://docs.hanko.io/api/public#tag/WebAuthn/operation/deleteCredential - */ - async deleteCredential(credentialID: string): Promise { - const response = await this.client.delete( - `/webauthn/credentials/${credentialID}` - ); - - if (response.status === 401) { - this.client.dispatcher.dispatchSessionExpiredEvent(); - throw new UnauthorizedError(); - } else if (!response.ok) { - throw new TechnicalError(); - } - - return; - } - - /** - * Determines whether a credential registration ceremony should be performed. Returns 'true' when WebAuthn - * is supported and the user's credentials do not intersect with the credentials already known on the - * current browser/device. - * - * @param {User} user - The user object. - * @return {Promise} - */ - async shouldRegister(user: User): Promise { - const supported = WebauthnSupport.supported(); - - if (!user.webauthn_credentials || !user.webauthn_credentials.length) { - return supported; - } - - const matches = this.webauthnState - .read() - .matchCredentials(user.id, user.webauthn_credentials); - - return supported && !matches.length; - } - - // eslint-disable-next-line require-jsdoc - _createAbortSignal() { - if (this.controller) { - this.controller.abort(); - } - - this.controller = new AbortController(); - return this.controller.signal; - } -} - -export { WebauthnClient }; diff --git a/frontend/frontend-sdk/src/lib/events/CustomEvents.ts b/frontend/frontend-sdk/src/lib/events/CustomEvents.ts index 718a4585a..ed4f98f1c 100644 --- a/frontend/frontend-sdk/src/lib/events/CustomEvents.ts +++ b/frontend/frontend-sdk/src/lib/events/CustomEvents.ts @@ -30,12 +30,18 @@ export const userLoggedOutType: "hanko-user-logged-out" = export const userDeletedType: "hanko-user-deleted" = "hanko-user-deleted"; /** - * The type of the `hanko-auth-flow-completed` event. - * @typedef {string} authFlowCompletedType + * The type of the `hanko-user-logged-in` event. + * @typedef {string} userLoggedInType * @memberOf Listener */ -export const authFlowCompletedType: "hanko-auth-flow-completed" = - "hanko-auth-flow-completed"; +export const userLoggedInType: "hanko-user-logged-in" = "hanko-user-logged-in"; + +/** + * The type of the `hanko-user-created` event. + * @typedef {string} userCreatedType + * @memberOf Listener + */ +export const userCreatedType: "hanko-user-created" = "hanko-user-created"; /** * The data passed in the `hanko-session-created` or `hanko-session-resumed` event. @@ -45,24 +51,10 @@ export const authFlowCompletedType: "hanko-auth-flow-completed" = * @subcategory Events * @property {string=} jwt - The JSON web token associated with the session. Only present when the Hanko-API allows the JWT to be accessible client-side. * @property {number} expirationSeconds - The number of seconds until the JWT expires. - * @property {string} userID - The user associated with the session. */ export interface SessionDetail { jwt?: string; expirationSeconds: number; - userID: string; -} - -/** - * The data passed in the `hanko-auth-flow-completed` event. - * - * @interface - * @category SDK - * @subcategory Events - * @property {string} userID - The user associated with the removed session. - */ -export interface AuthFlowCompletedDetail { - userID: string; } /** diff --git a/frontend/frontend-sdk/src/lib/events/Dispatcher.ts b/frontend/frontend-sdk/src/lib/events/Dispatcher.ts index f4736736f..87dee996a 100644 --- a/frontend/frontend-sdk/src/lib/events/Dispatcher.ts +++ b/frontend/frontend-sdk/src/lib/events/Dispatcher.ts @@ -1,11 +1,9 @@ import { SessionDetail, CustomEventWithDetail, - AuthFlowCompletedDetail, sessionCreatedType, sessionExpiredType, userDeletedType, - authFlowCompletedType, userLoggedOutType, } from "./CustomEvents"; import { SessionState } from "../state/session/SessionState"; @@ -77,14 +75,4 @@ export class Dispatcher { public dispatchUserDeletedEvent() { this.dispatch(userDeletedType, null); } - - /** - * Dispatches a "hanko-auth-flow-completed" event to the document with the specified detail. - * - * @param {AuthFlowCompletedDetail} detail - The event detail. - */ - public dispatchAuthFlowCompletedEvent(detail: AuthFlowCompletedDetail) { - this._sessionState.read().setAuthFlowCompleted(true).write(); - this.dispatch(authFlowCompletedType, detail); - } } diff --git a/frontend/frontend-sdk/src/lib/events/Listener.ts b/frontend/frontend-sdk/src/lib/events/Listener.ts index 7a873d884..ff13e108d 100644 --- a/frontend/frontend-sdk/src/lib/events/Listener.ts +++ b/frontend/frontend-sdk/src/lib/events/Listener.ts @@ -2,11 +2,9 @@ import { Throttle } from "../Throttle"; import { CustomEventWithDetail, SessionDetail, - AuthFlowCompletedDetail, sessionCreatedType, sessionExpiredType, userDeletedType, - authFlowCompletedType, userLoggedOutType, } from "./CustomEvents"; @@ -84,7 +82,7 @@ export class Listener { */ private wrapCallback( callback: CallbackFunc, - throttle: boolean + throttle: boolean, ): WrappedCallback { // The function that will be called when the event is triggered. const wrappedCallback = (event: CustomEventWithDetail) => { @@ -134,7 +132,7 @@ export class Listener { private static mapAddEventListenerParams( type: string, { once, callback }: EventListenerParams, - throttle?: boolean + throttle?: boolean, ): EventListenerWithTypeParams { return { type, @@ -156,10 +154,10 @@ export class Listener { private addEventListener( type: string, params: EventListenerParams, - throttle?: boolean + throttle?: boolean, ) { return this.addEventListenerWithType( - Listener.mapAddEventListenerParams(type, params, throttle) + Listener.mapAddEventListenerParams(type, params, throttle), ); } @@ -173,7 +171,7 @@ export class Listener { */ public onSessionCreated( callback: CallbackFunc, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(sessionCreatedType, { callback, once }, true); } @@ -189,7 +187,7 @@ export class Listener { */ public onSessionExpired( callback: CallbackFunc, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(sessionExpiredType, { callback, once }, true); } @@ -204,7 +202,7 @@ export class Listener { */ public onUserLoggedOut( callback: CallbackFunc, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(userLoggedOutType, { callback, once }); } @@ -218,22 +216,8 @@ export class Listener { */ public onUserDeleted( callback: CallbackFunc, - once?: boolean + once?: boolean, ): CleanupFunc { return this.addEventListener(userDeletedType, { callback, once }); } - - /** - * Adds an event listener for hanko-auth-flow-completed events. Will be triggered after the login or registration flow has been completed. - * - * @param {CallbackFunc} callback - The function to be called when the event is triggered. - * @param {boolean=} once - Whether the event listener should be removed after being called once. - * @returns {CleanupFunc} This function can be called to remove the event listener. - */ - public onAuthFlowCompleted( - callback: CallbackFunc, - once?: boolean - ): CleanupFunc { - return this.addEventListener(authFlowCompletedType, { callback, once }); - } } diff --git a/frontend/frontend-sdk/src/lib/events/Relay.ts b/frontend/frontend-sdk/src/lib/events/Relay.ts index 2c87d7a9b..d74227608 100644 --- a/frontend/frontend-sdk/src/lib/events/Relay.ts +++ b/frontend/frontend-sdk/src/lib/events/Relay.ts @@ -49,7 +49,7 @@ export class Relay extends Dispatcher { this._scheduler.scheduleTask( sessionExpiredType, () => this.dispatchSessionExpiredEvent(), - detail.expirationSeconds + detail.expirationSeconds, ); }; @@ -81,11 +81,6 @@ export class Relay extends Dispatcher { return; } - if (this._session.isAuthFlowCompleted()) { - this.dispatchAuthFlowCompletedEvent({ userID: sessionDetail.userID }); - return; - } - this.dispatchSessionCreatedEvent(sessionDetail); }; diff --git a/frontend/frontend-sdk/src/lib/flow-api/State.ts b/frontend/frontend-sdk/src/lib/flow-api/State.ts index ed9719435..469b20a58 100644 --- a/frontend/frontend-sdk/src/lib/flow-api/State.ts +++ b/frontend/frontend-sdk/src/lib/flow-api/State.ts @@ -160,9 +160,6 @@ class State validateAction(action); return action; }, - /** - * Safe version of `validate` that returns - */ tryValidate() { try { validateAction(action); diff --git a/frontend/frontend-sdk/src/lib/state/State.ts b/frontend/frontend-sdk/src/lib/state/State.ts index 6fde5476a..487e53c90 100644 --- a/frontend/frontend-sdk/src/lib/state/State.ts +++ b/frontend/frontend-sdk/src/lib/state/State.ts @@ -1,4 +1,3 @@ -import { LocalStorageUsers } from "./users/UserState"; import { LocalStorageSession } from "./session/SessionState"; /** @@ -8,7 +7,6 @@ import { LocalStorageSession } from "./session/SessionState"; * @property {LocalStorageUsers=} users - The user states. */ interface LocalStorage { - users?: LocalStorageUsers; session?: LocalStorageSession; } diff --git a/frontend/frontend-sdk/src/lib/state/session/SessionState.ts b/frontend/frontend-sdk/src/lib/state/session/SessionState.ts index 7ab1a9148..f6ed15bbe 100644 --- a/frontend/frontend-sdk/src/lib/state/session/SessionState.ts +++ b/frontend/frontend-sdk/src/lib/state/session/SessionState.ts @@ -19,7 +19,6 @@ interface SessionStateOptions { */ export interface LocalStorageSession { expiry: number; - userID: string; authFlowCompleted: boolean; } @@ -55,7 +54,7 @@ class SessionState extends State { * @return {LocalStorageSession} */ getState(): LocalStorageSession { - this.ls.session ||= { expiry: 0, userID: "", authFlowCompleted: false }; + this.ls.session ||= { expiry: 0, authFlowCompleted: false }; return this.ls.session; } @@ -79,24 +78,6 @@ class SessionState extends State { return this; } - /** - * Gets the user id. - */ - getUserID(): string { - return this.getState().userID; - } - - /** - * Sets the user id. - * - * @param {string} userID - The user id - * @return {SessionState} - */ - setUserID(userID: string): SessionState { - this.getState().userID = userID; - return this; - } - /** * Gets the authFlowCompleted indicator. */ @@ -124,7 +105,6 @@ class SessionState extends State { const session = this.getState(); delete session.expiry; - delete session.userID; return this; } diff --git a/frontend/frontend-sdk/src/lib/state/users/PasscodeState.ts b/frontend/frontend-sdk/src/lib/state/users/PasscodeState.ts deleted file mode 100644 index 5f503ab39..000000000 --- a/frontend/frontend-sdk/src/lib/state/users/PasscodeState.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { State } from "../State"; -import { UserState } from "./UserState"; - -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {string=} id - The UUID of the active passcode. - * @property {number=} ttl - Timestamp until when the passcode is valid in seconds (since January 1, 1970 00:00:00 UTC). - * @property {number=} resendAfter - Seconds until a passcode can be resent. - * @property {emailID=} emailID - The email address ID. - */ -export interface LocalStoragePasscode { - id?: string; - ttl?: number; - resendAfter?: number; - emailID?: string; -} - -/** - * A class that manages passcodes via local storage. - * - * @extends UserState - * @category SDK - * @subcategory Internal - */ -class PasscodeState extends UserState { - /** - * Get the passcode state. - * - * @private - * @param {string} userID - The UUID of the user. - * @return {LocalStoragePasscode} - */ - private getState(userID: string): LocalStoragePasscode { - return (super.getUserState(userID).passcode ||= {}); - } - - /** - * Reads the current state. - * - * @public - * @return {PasscodeState} - */ - read(): PasscodeState { - super.read(); - - return this; - } - - /** - * Gets the UUID of the active passcode. - * - * @param {string} userID - The UUID of the user. - * @return {string} - */ - getActiveID(userID: string): string { - return this.getState(userID).id; - } - - /** - * Sets the UUID of the active passcode. - * - * @param {string} userID - The UUID of the user. - * @param {string} passcodeID - The UUID of the passcode to be set as active. - * @return {PasscodeState} - */ - setActiveID(userID: string, passcodeID: string): PasscodeState { - this.getState(userID).id = passcodeID; - - return this; - } - - /** - * Gets the UUID of the email address. - * - * @param {string} userID - The UUID of the user. - * @return {string} - */ - getEmailID(userID: string): string { - return this.getState(userID).emailID; - } - - /** - * Sets the UUID of the email address. - * - * @param {string} userID - The UUID of the user. - * @param {string} emailID - The UUID of the email address. - * @return {PasscodeState} - */ - setEmailID(userID: string, emailID: string): PasscodeState { - this.getState(userID).emailID = emailID; - - return this; - } - - /** - * Removes the active passcode. - * - * @param {string} userID - The UUID of the user. - * @return {PasscodeState} - */ - reset(userID: string): PasscodeState { - const passcode = this.getState(userID); - - delete passcode.id; - delete passcode.ttl; - delete passcode.resendAfter; - delete passcode.emailID; - - return this; - } - - /** - * Gets the TTL in seconds. When the seconds expire, the code is invalid. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getTTL(userID: string): number { - return State.timeToRemainingSeconds(this.getState(userID).ttl); - } - - /** - * Sets the passcode's TTL and stores it to the local storage. - * - * @param {string} userID - The UUID of the user. - * @param {string} seconds - Number of seconds the passcode is valid for. - * @return {PasscodeState} - */ - setTTL(userID: string, seconds: number): PasscodeState { - this.getState(userID).ttl = State.remainingSecondsToTime(seconds); - - return this; - } - - /** - * Gets the number of seconds until when the next passcode can be sent. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getResendAfter(userID: string): number { - return State.timeToRemainingSeconds(this.getState(userID).resendAfter); - } - - /** - * Sets the number of seconds until a new passcode can be sent. - * - * @param {string} userID - The UUID of the user. - * @param {number} seconds - Number of seconds the passcode is valid for. - * @return {PasscodeState} - */ - setResendAfter(userID: string, seconds: number): PasscodeState { - this.getState(userID).resendAfter = State.remainingSecondsToTime(seconds); - - return this; - } -} - -export { PasscodeState }; diff --git a/frontend/frontend-sdk/src/lib/state/users/PasswordState.ts b/frontend/frontend-sdk/src/lib/state/users/PasswordState.ts deleted file mode 100644 index c615d45f6..000000000 --- a/frontend/frontend-sdk/src/lib/state/users/PasswordState.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { State } from "../State"; -import { UserState } from "./UserState"; - -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {number=} retryAfter - Timestamp (in seconds since January 1, 1970 00:00:00 UTC) indicating when the next password login can be attempted. - */ -export interface LocalStoragePassword { - retryAfter?: number; -} - -/** - * A class that manages the password login state. - * - * @extends UserState - * @category SDK - * @subcategory Internal - */ -class PasswordState extends UserState { - /** - * Get the password state. - * - * @private - * @param {string} userID - The UUID of the user. - * @return {LocalStoragePassword} - */ - private getState(userID: string): LocalStoragePassword { - return (super.getUserState(userID).password ||= {}); - } - - /** - * Reads the current state. - * - * @public - * @return {PasswordState} - */ - read(): PasswordState { - super.read(); - - return this; - } - - /** - * Gets the number of seconds until when a new password login can be attempted. - * - * @param {string} userID - The UUID of the user. - * @return {number} - */ - getRetryAfter(userID: string): number { - return State.timeToRemainingSeconds(this.getState(userID).retryAfter); - } - - /** - * Sets the number of seconds until a new password login can be attempted. - * - * @param {string} userID - The UUID of the user. - * @param {string} seconds - Number of seconds the passcode is valid for. - * @return {PasswordState} - */ - setRetryAfter(userID: string, seconds: number): PasswordState { - this.getState(userID).retryAfter = State.remainingSecondsToTime(seconds); - - return this; - } -} - -export { PasswordState }; diff --git a/frontend/frontend-sdk/src/lib/state/users/UserState.ts b/frontend/frontend-sdk/src/lib/state/users/UserState.ts deleted file mode 100644 index af2820b69..000000000 --- a/frontend/frontend-sdk/src/lib/state/users/UserState.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {Object.} - A dictionary for mapping users to their states. - */ -import { State } from "../State"; - -import { LocalStorageWebauthn } from "./WebauthnState"; -import { LocalStoragePasscode } from "./PasscodeState"; -import { LocalStoragePassword } from "./PasswordState"; - -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {LocalStorageWebauthn=} webauthn - Information about WebAuthn credentials. - * @property {LocalStoragePasscode=} passcode - Information about the active passcode. - * @property {LocalStoragePassword=} password - Information about the password login attempts. - */ -interface LocalStorageUser { - webauthn?: LocalStorageWebauthn; - passcode?: LocalStoragePasscode; - password?: LocalStoragePassword; -} - -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {Object.} - A dictionary for mapping users to their states. - */ -export interface LocalStorageUsers { - [userID: string]: LocalStorageUser; -} - -/** - * A class to read and write local storage contents. - * - * @abstract - * @extends State - * @param {string} key - The local storage key. - * @category SDK - * @subcategory Internal - */ -abstract class UserState extends State { - /** - * Gets the state of the specified user. - * - * @param {string} userID - The UUID of the user. - * @return {LocalStorageUser} - */ - getUserState(userID: string): LocalStorageUser { - this.ls.users ||= {}; - - if (!Object.prototype.hasOwnProperty.call(this.ls.users, userID)) { - this.ls.users[userID] = {}; - } - - return this.ls.users[userID]; - } -} - -export { UserState }; diff --git a/frontend/frontend-sdk/src/lib/state/users/WebauthnState.ts b/frontend/frontend-sdk/src/lib/state/users/WebauthnState.ts deleted file mode 100644 index d81026cae..000000000 --- a/frontend/frontend-sdk/src/lib/state/users/WebauthnState.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { UserState } from "./UserState"; -import { Credential } from "../../Dto"; - -/** - * @interface - * @category SDK - * @subcategory Internal - * @property {string[]?} credentials - A list of known credential IDs on the current browser. - */ -export interface LocalStorageWebauthn { - credentials?: string[]; -} - -/** - * A class that manages WebAuthn credentials via local storage. - * - * @extends UserState - * @category SDK - * @subcategory Internal - */ -class WebauthnState extends UserState { - /** - * Gets the WebAuthn state. - * - * @private - * @param {string} userID - The UUID of the user. - * @return {LocalStorageWebauthn} - */ - private getState(userID: string): LocalStorageWebauthn { - return (super.getUserState(userID).webauthn ||= {}); - } - - /** - * Reads the current state. - * - * @public - * @return {WebauthnState} - */ - read(): WebauthnState { - super.read(); - - return this; - } - - /** - * Gets the list of known credentials on the current browser. - * - * @param {string} userID - The UUID of the user. - * @return {string[]} - */ - getCredentials(userID: string): string[] { - return (this.getState(userID).credentials ||= []); - } - - /** - * Adds the credential to the list of known credentials. - * - * @param {string} userID - The UUID of the user. - * @param {string} credentialID - The WebAuthn credential ID. - * @return {WebauthnState} - */ - addCredential(userID: string, credentialID: string): WebauthnState { - this.getCredentials(userID).push(credentialID); - - return this; - } - - /** - * Returns the intersection between the specified list of credentials and the known credentials stored in - * the local storage. - * - * @param {string} userID - The UUID of the user. - * @param {Credential[]} match - A list of credential IDs to be matched against the local storage. - * @return {Credential[]} - */ - matchCredentials(userID: string, match: Credential[]): Credential[] { - return this.getCredentials(userID) - .filter((id) => match.find((c) => c.id === id)) - .map((id: string) => ({ id } as Credential)); - } -} - -export { WebauthnState }; diff --git a/frontend/frontend-sdk/tests/Hanko.spec.ts b/frontend/frontend-sdk/tests/Hanko.spec.ts index 195ef5c79..eebc028d5 100644 --- a/frontend/frontend-sdk/tests/Hanko.spec.ts +++ b/frontend/frontend-sdk/tests/Hanko.spec.ts @@ -1,22 +1,10 @@ -import { - ConfigClient, - EnterpriseClient, - Hanko, - PasscodeClient, - PasswordClient, - UserClient, - WebauthnClient, -} from "../src"; +import { EnterpriseClient, Hanko, UserClient } from "../src"; describe("class hanko", () => { it("should hold instances of available Hanko API clients", async () => { const hanko = new Hanko("http://api.test"); - expect(hanko.config).toBeInstanceOf(ConfigClient); expect(hanko.user).toBeInstanceOf(UserClient); - expect(hanko.passcode).toBeInstanceOf(PasscodeClient); - expect(hanko.password).toBeInstanceOf(PasswordClient); - expect(hanko.webauthn).toBeInstanceOf(WebauthnClient); expect(hanko.enterprise).toBeInstanceOf(EnterpriseClient); }); }); diff --git a/frontend/frontend-sdk/tests/lib/Session.spec.ts b/frontend/frontend-sdk/tests/lib/Session.spec.ts index 2bd054b42..6f3e339b2 100644 --- a/frontend/frontend-sdk/tests/lib/Session.spec.ts +++ b/frontend/frontend-sdk/tests/lib/Session.spec.ts @@ -12,16 +12,12 @@ describe("Session", () => { it("should return session details if valid", () => { // Prepare const expectedDetails = { - userID: "12345", expirationSeconds: 3600, jwt: "some.jwt.token", }; // Mock dependencies jest.spyOn(session._sessionState, "read").mockImplementation(); - jest - .spyOn(session._sessionState, "getUserID") - .mockReturnValue(expectedDetails.userID); jest .spyOn(session._sessionState, "getExpirationSeconds") .mockReturnValue(expectedDetails.expirationSeconds); @@ -39,16 +35,12 @@ describe("Session", () => { it("should return null if session details are invalid", () => { // Prepare const invalidDetails: SessionDetail = { - userID: "", expirationSeconds: 0, jwt: null, }; // Mock dependencies jest.spyOn(session._sessionState, "read").mockImplementation(); - jest - .spyOn(session._sessionState, "getUserID") - .mockReturnValue(invalidDetails.userID); jest .spyOn(session._sessionState, "getExpirationSeconds") .mockReturnValue(invalidDetails.expirationSeconds); @@ -86,7 +78,6 @@ describe("Session", () => { it("should return false if the user is not logged in", () => { // Prepare const notLoggedInDetails: SessionDetail = { - userID: "", expirationSeconds: 0, jwt: null, }; diff --git a/frontend/frontend-sdk/tests/lib/client/ConfigClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/ConfigClient.spec.ts deleted file mode 100644 index 93a702635..000000000 --- a/frontend/frontend-sdk/tests/lib/client/ConfigClient.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ConfigClient, TechnicalError } from "../../../src"; -import { Response } from "../../../src/lib/client/HttpClient"; - -let configClient: ConfigClient; - -beforeEach(() => { - configClient = new ConfigClient("http://test.api", { - cookieName: "hanko", - localStorageKey: "hanko", - timeout: 13000, - }); -}); - -describe("configClient.get()", () => { - it("should call well-known config endpoint and return config", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - response._decodedJSON = { password: { enabled: true } }; - - jest.spyOn(configClient.client, "get").mockResolvedValue(response); - const config = await configClient.get(); - expect(configClient.client.get).toHaveBeenCalledWith("/.well-known/config"); - expect(config).toEqual(response._decodedJSON); - }); - - it("should throw technical error when API response is not ok", async () => { - const response = new Response(new XMLHttpRequest()); - configClient.client.get = jest.fn().mockResolvedValue(response); - - const config = configClient.get(); - await expect(config).rejects.toThrow(TechnicalError); - }); - - it("should throw error on API communication failure", async () => { - configClient.client.get = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const config = configClient.get(); - await expect(config).rejects.toThrowError("Test error"); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/client/EmailClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/EmailClient.spec.ts index f684eb4a6..0b3991caa 100644 --- a/frontend/frontend-sdk/tests/lib/client/EmailClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/EmailClient.spec.ts @@ -47,7 +47,7 @@ describe("EmailClient.list()", () => { const email = emailClient.list(); await expect(email).rejects.toThrow(error); - } + }, ); it("should throw error on API communication failure", async () => { @@ -98,7 +98,7 @@ describe("EmailClient.create()", () => { const email = emailClient.create(emailAddress); await expect(email).rejects.toThrow(error); - } + }, ); it("should throw error on API communication failure", async () => { @@ -119,7 +119,7 @@ describe("EmailClient.setPrimaryEmail()", () => { jest.spyOn(emailClient.client, "post").mockResolvedValue(response); const update = await emailClient.setPrimaryEmail(emailID); expect(emailClient.client.post).toHaveBeenCalledWith( - `/emails/${emailID}/set_primary` + `/emails/${emailID}/set_primary`, ); expect(update).toEqual(undefined); }); @@ -139,7 +139,7 @@ describe("EmailClient.setPrimaryEmail()", () => { const email = emailClient.setPrimaryEmail(emailID); await expect(email).rejects.toThrow(error); - } + }, ); it("should throw error on API communication failure", async () => { @@ -160,7 +160,7 @@ describe("EmailClient.delete()", () => { jest.spyOn(emailClient.client, "delete").mockResolvedValue(response); const deleteResponse = await emailClient.delete(emailID); expect(emailClient.client.delete).toHaveBeenCalledWith( - `/emails/${emailID}` + `/emails/${emailID}`, ); expect(deleteResponse).toEqual(undefined); }); @@ -180,7 +180,7 @@ describe("EmailClient.delete()", () => { const deleteResponse = emailClient.delete(emailID); await expect(deleteResponse).rejects.toThrow(error); - } + }, ); it("should throw error on API communication failure", async () => { diff --git a/frontend/frontend-sdk/tests/lib/client/EnterpriseClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/EnterpriseClient.spec.ts index 6d831e8db..e7de9ff65 100644 --- a/frontend/frontend-sdk/tests/lib/client/EnterpriseClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/EnterpriseClient.spec.ts @@ -1,4 +1,9 @@ -import {EnterpriseClient, NotFoundError, TechnicalError, ThirdPartyError} from '../../../src'; +import { + EnterpriseClient, + NotFoundError, + TechnicalError, + ThirdPartyError, +} from "../../../src"; import { Response } from "../../../src/lib/client/HttpClient"; let enterpriseClient: EnterpriseClient; diff --git a/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts index 0ccf34aa5..ec8c00472 100644 --- a/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts @@ -162,7 +162,6 @@ describe("httpClient.processResponseHeadersOnLogin()", () => { describe("when the x-auth-token is available in the response header", () => { const jwt = "test-jwt"; const expirationSeconds = 7; - const userID = "test-user"; const realLocation = window.location; beforeEach(() => { @@ -219,34 +218,25 @@ describe("httpClient.processResponseHeadersOnLogin()", () => { }; jest.spyOn(response.xhr, "getResponseHeader"); - jest.spyOn(client.passcodeState, "read"); - jest.spyOn(client.passcodeState, "reset"); - jest.spyOn(client.passcodeState, "write"); jest.spyOn(client.sessionState, "read"); jest.spyOn(client.cookie, "setAuthCookie"); jest.spyOn(client.sessionState, "setExpirationSeconds"); - jest.spyOn(client.sessionState, "setUserID"); jest.spyOn(client.sessionState, "write"); - client.processResponseHeadersOnLogin(userID, response); + client.processHeaders(xhr); expect(response.xhr.getResponseHeader).toBeCalledTimes(2); - expect(client.passcodeState.read).toBeCalledTimes(1); - expect(client.passcodeState.reset).toBeCalledTimes(1); - expect(client.passcodeState.write).toBeCalledTimes(1); expect(client.cookie.setAuthCookie).toHaveBeenCalledTimes(1); expect(client.sessionState.read).toHaveBeenCalledTimes(1); expect(client.sessionState.setExpirationSeconds).toHaveBeenCalledTimes( 1, ); - expect(client.sessionState.setUserID).toHaveBeenCalledTimes(1); expect(client.sessionState.write).toHaveBeenCalledTimes(1); expect(client.sessionState.setExpirationSeconds).toHaveBeenCalledWith( expirationSeconds, ); - expect(client.sessionState.setUserID).toHaveBeenCalledWith(userID); expect(client.cookie.setAuthCookie).toHaveBeenCalledWith(jwt, { secure, diff --git a/frontend/frontend-sdk/tests/lib/client/PasscodeClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/PasscodeClient.spec.ts deleted file mode 100644 index ee07f85d8..000000000 --- a/frontend/frontend-sdk/tests/lib/client/PasscodeClient.spec.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { - InvalidPasscodeError, - MaxNumOfPasscodeAttemptsReachedError, - PasscodeClient, - PasscodeExpiredError, - TechnicalError, - TooManyRequestsError, -} from "../../../src"; -import { Response } from "../../../src/lib/client/HttpClient"; - -const userID = "test-user-1"; -const passcodeID = "test-passcode-1"; -const emailID = "test-email-1"; -const passcodeTTL = 180; -const passcodeRetryAfter = 180; -const passcodeValue = "123456"; -let passcodeClient: PasscodeClient; - -beforeEach(() => { - passcodeClient = new PasscodeClient("http://test.api", { - cookieName: "hanko", - localStorageKey: "hanko", - timeout: 13000, - }); -}); - -describe("PasscodeClient.initialize()", () => { - it("should initialize a passcode login", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - response._decodedJSON = { id: passcodeID, ttl: passcodeTTL }; - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.state, "setTTL"); - jest.spyOn(passcodeClient.state, "setActiveID"); - jest.spyOn(passcodeClient.state, "write"); - - const passcode = await passcodeClient.initialize(userID); - expect(passcode.id).toEqual(passcodeID); - expect(passcode.ttl).toEqual(passcodeTTL); - - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.setTTL).toHaveBeenCalledWith( - userID, - passcodeTTL - ); - expect(passcodeClient.state.setActiveID).toHaveBeenCalledWith( - userID, - passcodeID - ); - expect(passcodeClient.state.write).toHaveBeenCalledTimes(1); - expect(passcodeClient.client.post).toHaveBeenCalledWith( - "/passcode/login/initialize", - { user_id: userID } - ); - }); - - it("should initialize a passcode with specified email id", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - jest.spyOn(passcodeClient.state, "setEmailID"); - - await passcodeClient.initialize(userID, emailID, true); - - expect(passcodeClient.state.setEmailID).toHaveBeenCalledWith( - userID, - emailID - ); - expect(passcodeClient.client.post).toHaveBeenCalledWith( - "/passcode/login/initialize", - { user_id: userID, email_id: emailID } - ); - }); - - it("should restore the previous passcode", async () => { - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID); - jest.spyOn(passcodeClient.state, "getEmailID").mockReturnValue(emailID); - - await expect(passcodeClient.initialize(userID, emailID)).resolves.toEqual({ - id: passcodeID, - ttl: passcodeTTL, - }); - - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.getTTL).toHaveBeenCalledWith(userID); - expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID); - expect(passcodeClient.state.getEmailID).toHaveBeenCalledWith(userID); - }); - - it("should throw an error as long as email backoff is active", async () => { - jest - .spyOn(passcodeClient.state, "getResendAfter") - .mockReturnValue(passcodeRetryAfter); - - await expect(passcodeClient.initialize(userID, emailID)).rejects.toThrow( - TooManyRequestsError - ); - - expect(passcodeClient.state.getResendAfter).toHaveBeenCalledWith(userID); - }); - - it("should throw error and set retry after in state on too many request response from API", async () => { - const xhr = new XMLHttpRequest(); - const response = new Response(xhr); - - response.status = 429; - - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - jest - .spyOn(response.headers, "getResponseHeader") - .mockReturnValue(`${passcodeRetryAfter}`); - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.state, "setResendAfter"); - jest.spyOn(passcodeClient.state, "write"); - - await expect(passcodeClient.initialize(userID)).rejects.toThrowError( - TooManyRequestsError - ); - - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.setResendAfter).toHaveBeenCalledWith( - userID, - passcodeRetryAfter - ); - expect(passcodeClient.state.write).toHaveBeenCalledTimes(1); - expect(response.headers.getResponseHeader).toHaveBeenCalledWith( - "Retry-After" - ); - }); - - it.each` - status | error - ${401} | ${"Technical error"} - ${500} | ${"Technical error"} - ${429} | ${"Too many requests error"} - `( - "should throw error when API response is not ok", - async ({ status, error }) => { - const response = new Response(new XMLHttpRequest()); - response.status = status; - response.ok = status >= 200 && status <= 299; - - passcodeClient.client.post = jest.fn().mockResolvedValue(response); - - const passcode = passcodeClient.initialize("test-user-1"); - await expect(passcode).rejects.toThrowError(error); - } - ); - - it("should throw error on API communication failure", async () => { - passcodeClient.client.post = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const passcode = passcodeClient.initialize("test-user-1"); - await expect(passcode).rejects.toThrowError("Test error"); - }); -}); - -describe("PasscodeClient.finalize()", () => { - it("should finalize a passcode login", async () => { - Object.defineProperty(global, "XMLHttpRequest", { - value: jest.fn().mockImplementation(() => ({ - response: JSON.stringify({ foo: "bar" }), - open: jest.fn(), - setRequestHeader: jest.fn(), - getResponseHeader: jest.fn(), - getAllResponseHeaders: jest.fn().mockReturnValue(""), - send: jest.fn(), - })), - configurable: true, - writable: true, - }); - - const response = new Response(new XMLHttpRequest()); - response.ok = true; - - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.client, "processResponseHeadersOnLogin"); - jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - - await expect( - passcodeClient.finalize(userID, passcodeValue) - ).resolves.toBeUndefined(); - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect( - passcodeClient.client.processResponseHeadersOnLogin - ).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID); - expect(passcodeClient.client.post).toHaveBeenCalledWith( - "/passcode/login/finalize", - { id: passcodeID, code: passcodeValue } - ); - }); - - it("should throw error when using an invalid passcode", async () => { - const response = new Response(new XMLHttpRequest()); - response.status = 401; - - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - - await expect( - passcodeClient.finalize(userID, passcodeValue) - ).rejects.toThrow(InvalidPasscodeError); - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID); - }); - - it("should throw error when reaching max passcode attempts", async () => { - const response = new Response(new XMLHttpRequest()); - response.status = 410; - - jest.spyOn(passcodeClient.state, "read"); - jest.spyOn(passcodeClient.state, "reset"); - jest.spyOn(passcodeClient.state, "write"); - jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response); - - await expect( - passcodeClient.finalize(userID, passcodeValue) - ).rejects.toThrow(MaxNumOfPasscodeAttemptsReachedError); - expect(passcodeClient.state.read).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.reset).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.write).toHaveBeenCalledTimes(1); - expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID); - }); - - it("should throw error when the passcode has expired", async () => { - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(0); - const finalizeResponse = passcodeClient.finalize(userID, passcodeValue); - await expect(finalizeResponse).rejects.toThrowError(PasscodeExpiredError); - }); - - it("should throw error when API response is not ok", async () => { - const response = new Response(new XMLHttpRequest()); - passcodeClient.client.post = jest.fn().mockResolvedValue(response); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - - const finalizeResponse = passcodeClient.finalize(userID, passcodeValue); - await expect(finalizeResponse).rejects.toThrowError(TechnicalError); - }); - - it("should throw error on API communication failure", async () => { - passcodeClient.client.post = jest - .fn() - .mockRejectedValue(new Error("Test error")); - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - - const finalizeResponse = passcodeClient.finalize(userID, passcodeValue); - await expect(finalizeResponse).rejects.toThrowError("Test error"); - }); -}); - -describe("PasscodeClient.getTTL()", () => { - it("should return passcode TTL", async () => { - jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL); - expect(passcodeClient.getTTL(userID)).toEqual(passcodeTTL); - }); -}); - -describe("PasscodeClient.getResendAfter()", () => { - it("should return passcode resend after seconds", async () => { - jest - .spyOn(passcodeClient.state, "getResendAfter") - .mockReturnValue(passcodeRetryAfter); - expect(passcodeClient.getResendAfter(userID)).toEqual(passcodeRetryAfter); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/client/PasswordClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/PasswordClient.spec.ts deleted file mode 100644 index 2595aa461..000000000 --- a/frontend/frontend-sdk/tests/lib/client/PasswordClient.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - PasswordClient, - InvalidPasswordError, - TooManyRequestsError, - TechnicalError, -} from "../../../src"; -import { Response } from "../../../src/lib/client/HttpClient"; - -const userID = "test-user-1"; -const password = "test-password-1"; -const passwordRetryAfter = 180; -let passwordClient: PasswordClient; - -beforeEach(() => { - passwordClient = new PasswordClient("http://test.api", { - cookieName: "hanko", - localStorageKey: "hanko", - timeout: 13000, - }); -}); - -describe("PasswordClient.login()", () => { - it("should do a password login", async () => { - Object.defineProperty(global, "XMLHttpRequest", { - value: jest.fn().mockImplementation(() => ({ - response: JSON.stringify({ foo: "bar" }), - open: jest.fn(), - setRequestHeader: jest.fn(), - getResponseHeader: jest.fn(), - getAllResponseHeaders: jest.fn().mockReturnValue(""), - send: jest.fn(), - })), - configurable: true, - writable: true, - }); - - const response = new Response(new XMLHttpRequest()); - response.ok = true; - jest.spyOn(passwordClient.client, "post").mockResolvedValue(response); - jest.spyOn(passwordClient.client, "processResponseHeadersOnLogin"); - - const loginResponse = passwordClient.login(userID, password); - await expect(loginResponse).resolves.toBeUndefined(); - expect( - passwordClient.client.processResponseHeadersOnLogin - ).toHaveBeenCalledTimes(1); - expect(passwordClient.client.post).toHaveBeenCalledWith("/password/login", { - user_id: userID, - password, - }); - }); - - it("should throw error when using an invalid password", async () => { - const response = new Response(new XMLHttpRequest()); - response.status = 401; - jest.spyOn(passwordClient.client, "post").mockResolvedValue(response); - - const loginResponse = passwordClient.login(userID, password); - await expect(loginResponse).rejects.toThrow(InvalidPasswordError); - }); - - it("should throw error and set retry after in state on too many request response from API", async () => { - const xhr = new XMLHttpRequest(); - const response = new Response(xhr); - - response.status = 429; - - jest.spyOn(passwordClient.client, "post").mockResolvedValue(response); - jest - .spyOn(response.headers, "getResponseHeader") - .mockReturnValue(`${passwordRetryAfter}`); - jest.spyOn(passwordClient.passwordState, "read"); - jest.spyOn(passwordClient.passwordState, "setRetryAfter"); - jest.spyOn(passwordClient.passwordState, "write"); - - await expect(passwordClient.login(userID, password)).rejects.toThrowError( - TooManyRequestsError - ); - - expect(passwordClient.passwordState.read).toHaveBeenCalledTimes(1); - expect(passwordClient.passwordState.setRetryAfter).toHaveBeenCalledWith( - userID, - passwordRetryAfter - ); - expect(passwordClient.passwordState.write).toHaveBeenCalledTimes(1); - expect(response.headers.getResponseHeader).toHaveBeenCalledWith( - "Retry-After" - ); - }); - - it("should throw error when API response is not ok", async () => { - const response = new Response(new XMLHttpRequest()); - passwordClient.client.post = jest.fn().mockResolvedValue(response); - - const loginResponse = passwordClient.login(userID, password); - await expect(loginResponse).rejects.toThrowError(TechnicalError); - }); - - it("should throw error on API communication failure", async () => { - passwordClient.client.post = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const loginResponse = passwordClient.login(userID, password); - await expect(loginResponse).rejects.toThrowError("Test error"); - }); -}); - -describe("PasswordClient.update()", () => { - it("should update a password", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - jest.spyOn(passwordClient.client, "put").mockResolvedValue(response); - - const loginResponse = passwordClient.update(userID, password); - await expect(loginResponse).resolves.toBeUndefined(); - - expect(passwordClient.client.put).toHaveBeenCalledWith("/password", { - user_id: userID, - password, - }); - }); - - it.each` - status | error - ${401} | ${"Unauthorized error"} - ${500} | ${"Technical error"} - `( - "should throw error when API response is not ok", - async ({ status, error }) => { - const response = new Response(new XMLHttpRequest()); - response.ok = status >= 200 && status <= 299; - response.status = status; - passwordClient.client.put = jest.fn().mockResolvedValue(response); - - const config = passwordClient.update(userID, password); - await expect(config).rejects.toThrowError(error); - } - ); - - it("should throw error on API communication failure", async () => { - passwordClient.client.put = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const config = passwordClient.update(userID, password); - await expect(config).rejects.toThrowError("Test error"); - }); - - describe("PasswordClient.getRetryAfter()", () => { - it("should return password resend after seconds", async () => { - jest - .spyOn(passwordClient.passwordState, "getRetryAfter") - .mockReturnValue(passwordRetryAfter); - expect(passwordClient.getRetryAfter(userID)).toEqual(passwordRetryAfter); - }); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/client/ThirdPartyClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/ThirdPartyClient.spec.ts index a98e82f77..e42f035e2 100644 --- a/frontend/frontend-sdk/tests/lib/client/ThirdPartyClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/ThirdPartyClient.spec.ts @@ -26,21 +26,21 @@ describe("thirdPartyClient.auth()", () => { it("should throw if provider is empty", async () => { await expect( - thirdPartyClient.auth("", "http://test.example") + thirdPartyClient.auth("", "http://test.example"), ).rejects.toThrow(ThirdPartyError); expect(window.location.assign).not.toHaveBeenCalled(); }); it("should throw if redirectTo is empty", async () => { await expect(thirdPartyClient.auth("testProvider", "")).rejects.toThrow( - ThirdPartyError + ThirdPartyError, ); expect(window.location.assign).not.toHaveBeenCalled(); }); it("should construct correct redirect url with provider", async () => { await expect( - thirdPartyClient.auth("testProvider", "http://test.example") + thirdPartyClient.auth("testProvider", "http://test.example"), ).resolves.not.toThrow(); const expectedUrl = "http://test.api/thirdparty/auth?provider=testProvider&redirect_to=http%3A%2F%2Ftest.example"; diff --git a/frontend/frontend-sdk/tests/lib/client/TokenClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/TokenClient.spec.ts index 081ce2d25..6ed2ae7bd 100644 --- a/frontend/frontend-sdk/tests/lib/client/TokenClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/TokenClient.spec.ts @@ -79,7 +79,7 @@ describe("tokenClient.validate()", () => { expect(window.history.replaceState).toHaveBeenCalledWith( null, null, - "/callback" + "/callback", ); }); }); diff --git a/frontend/frontend-sdk/tests/lib/client/UserClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/UserClient.spec.ts index 245570970..e6b695b54 100644 --- a/frontend/frontend-sdk/tests/lib/client/UserClient.spec.ts +++ b/frontend/frontend-sdk/tests/lib/client/UserClient.spec.ts @@ -95,7 +95,7 @@ describe("UserClient.getCurrent()", () => { expect(userClient.client.get).toHaveBeenNthCalledWith(1, "/me"); expect(userClient.client.get).toHaveBeenNthCalledWith( 2, - `/users/${userID}` + `/users/${userID}`, ); }); @@ -127,7 +127,7 @@ describe("UserClient.getCurrent()", () => { const user = userClient.getCurrent(); await expect(user).rejects.toThrow(error); - } + }, ); it("should throw error on API communication failure", async () => { @@ -240,7 +240,7 @@ describe("UserClient.logout()", () => { await expect(userClient.logout()).rejects.toThrow(error); expect(userClient.client.post).toHaveBeenCalledWith("/logout"); - } + }, ); }); @@ -273,6 +273,6 @@ describe("UserClient.delete()", () => { await expect(userClient.delete()).rejects.toThrow(error); expect(userClient.client.delete).toHaveBeenCalledWith("/user"); - } + }, ); }); diff --git a/frontend/frontend-sdk/tests/lib/client/WebauthnClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/WebauthnClient.spec.ts deleted file mode 100644 index c1ca1628a..000000000 --- a/frontend/frontend-sdk/tests/lib/client/WebauthnClient.spec.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { - User, - WebauthnClient, - WebauthnRequestCancelledError, - WebauthnSupport, -} from "../../../src"; -import { Response } from "../../../src/lib/client/HttpClient"; - -import { PublicKeyCredentialWithAssertionJSON } from "@github/webauthn-json"; -import { Attestation } from "../../../src/lib/Dto"; - -const userID = "test-user-1"; -const credentialID = "credential-1"; - -let webauthnClient: WebauthnClient; - -beforeEach(() => { - webauthnClient = new WebauthnClient("http://test.api", { - cookieName: "hanko", - localStorageKey: "hanko", - timeout: 13000, - }); -}); - -describe("webauthnClient.login()", () => { - const fakeRequestOptions = {} as PublicKeyCredentialRequestOptions; - const fakeAssertion = {} as PublicKeyCredentialWithAssertionJSON; - - it("should perform a webauthn login", async () => { - Object.defineProperty(global, "XMLHttpRequest", { - value: jest.fn().mockImplementation(() => ({ - response: JSON.stringify({ foo: "bar" }), - open: jest.fn(), - setRequestHeader: jest.fn(), - getResponseHeader: jest.fn(), - getAllResponseHeaders: jest.fn().mockReturnValue(""), - send: jest.fn(), - })), - configurable: true, - writable: true, - }); - - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = true; - initResponse._decodedJSON = fakeRequestOptions; - - const finalResponse = new Response(new XMLHttpRequest()); - finalResponse.ok = true; - finalResponse._decodedJSON = { - user_id: userID, - credential_id: credentialID, - }; - - webauthnClient._getCredential = jest.fn().mockResolvedValue(fakeAssertion); - webauthnClient._createAbortSignal = jest.fn(); - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse) - .mockResolvedValueOnce(finalResponse); - - jest.spyOn(webauthnClient.webauthnState, "read"); - jest.spyOn(webauthnClient.webauthnState, "addCredential"); - jest.spyOn(webauthnClient.webauthnState, "write"); - jest.spyOn(webauthnClient.client, "processResponseHeadersOnLogin"); - - await webauthnClient.login(userID, true); - - expect(webauthnClient._getCredential).toHaveBeenCalledWith({ - ...fakeRequestOptions, - mediation: "conditional", - }); - expect(webauthnClient._createAbortSignal).toHaveBeenCalledTimes(1); - expect(webauthnClient.webauthnState.read).toHaveBeenCalledTimes(1); - expect(webauthnClient.webauthnState.addCredential).toHaveBeenCalledWith( - userID, - credentialID - ); - expect(webauthnClient.webauthnState.write).toHaveBeenCalledTimes(1); - expect( - webauthnClient.client.processResponseHeadersOnLogin - ).toHaveBeenCalledTimes(1); - expect(webauthnClient.client.post).toHaveBeenNthCalledWith( - 1, - "/webauthn/login/initialize", - { user_id: userID } - ); - expect(webauthnClient.client.post).toHaveBeenNthCalledWith( - 2, - "/webauthn/login/finalize", - fakeAssertion - ); - }); - - it.each` - statusInit | statusFinal | error - ${500} | ${200} | ${"Technical error"} - ${200} | ${400} | ${"Invalid WebAuthn credential error"} - ${200} | ${401} | ${"Invalid WebAuthn credential error"} - ${200} | ${500} | ${"Technical error"} - `( - "should throw error if API returns an error status", - async ({ statusInit, statusFinal, error }) => { - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = statusInit >= 200 && statusInit <= 299; - initResponse.status = statusInit; - initResponse._decodedJSON = fakeRequestOptions; - - const finalResponse = new Response(new XMLHttpRequest()); - finalResponse.ok = statusFinal >= 200 && statusFinal <= 299; - finalResponse.status = statusFinal; - finalResponse._decodedJSON = { - user_id: userID, - credential_id: credentialID, - }; - - webauthnClient._getCredential = jest - .fn() - .mockResolvedValue({} as PublicKeyCredentialWithAssertionJSON); - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse) - .mockResolvedValueOnce(finalResponse); - - const user = webauthnClient.login(); - await expect(user).rejects.toThrow(error); - } - ); - - it("should throw an error when the WebAuthn API call fails", async () => { - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = true; - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse); - - webauthnClient._getCredential = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const user = webauthnClient.login(); - await expect(user).rejects.toThrow(WebauthnRequestCancelledError); - }); -}); - -describe("webauthnClient.register()", () => { - const fakeCreationOptions = {} as PublicKeyCredentialCreationOptions; - const fakeAttestation = { response: { transports: [] } } as Attestation; - - it("should perform a webauthn registration", async () => { - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = true; - initResponse._decodedJSON = fakeCreationOptions; - - const finalResponse = new Response(new XMLHttpRequest()); - finalResponse.ok = true; - finalResponse._decodedJSON = { - user_id: userID, - credential_id: credentialID, - }; - - webauthnClient._createCredential = jest - .fn() - .mockResolvedValue(fakeAttestation); - webauthnClient._createAbortSignal = jest.fn(); - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse) - .mockResolvedValueOnce(finalResponse); - - jest.spyOn(webauthnClient.webauthnState, "read"); - jest.spyOn(webauthnClient.webauthnState, "addCredential"); - jest.spyOn(webauthnClient.webauthnState, "write"); - - await webauthnClient.register(); - - expect(webauthnClient._createCredential).toHaveBeenCalledWith({ - ...fakeCreationOptions, - }); - expect(webauthnClient._createAbortSignal).toHaveBeenCalledTimes(1); - expect(webauthnClient.webauthnState.read).toHaveBeenCalledTimes(1); - expect(webauthnClient.webauthnState.addCredential).toHaveBeenCalledWith( - userID, - credentialID - ); - expect(webauthnClient.webauthnState.write).toHaveBeenCalledTimes(1); - expect(webauthnClient.client.post).toHaveBeenNthCalledWith( - 1, - "/webauthn/registration/initialize" - ); - expect(webauthnClient.client.post).toHaveBeenNthCalledWith( - 2, - "/webauthn/registration/finalize", - fakeAttestation - ); - }); - - it.each` - statusInit | statusFinal | error - ${401} | ${200} | ${"Unauthorized error"} - ${500} | ${200} | ${"Technical error"} - ${200} | ${401} | ${"Unauthorized error"} - ${200} | ${422} | ${"User verification error"} - ${200} | ${500} | ${"Technical error"} - `( - "should throw error if API returns an error status", - async ({ statusInit, statusFinal, error }) => { - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = statusInit >= 200 && statusInit <= 299; - initResponse.status = statusInit; - initResponse._decodedJSON = fakeCreationOptions; - - const finalResponse = new Response(new XMLHttpRequest()); - finalResponse.ok = statusFinal >= 200 && statusFinal <= 299; - finalResponse.status = statusFinal; - finalResponse._decodedJSON = { - user_id: userID, - credential_id: credentialID, - }; - - webauthnClient._createCredential = jest - .fn() - .mockResolvedValue(fakeAttestation); - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse) - .mockResolvedValueOnce(finalResponse); - - const user = webauthnClient.register(); - await expect(user).rejects.toThrow(error); - } - ); - - it("should throw an error when the WebAuthn API call fails", async () => { - const initResponse = new Response(new XMLHttpRequest()); - initResponse.ok = true; - - jest - .spyOn(webauthnClient.client, "post") - .mockResolvedValueOnce(initResponse); - - webauthnClient._createCredential = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const user = webauthnClient.register(); - await expect(user).rejects.toThrow(WebauthnRequestCancelledError); - }); -}); - -describe("webauthnClient.shouldRegister()", () => { - it.each` - isSupported | userHasCredential | credentialMatched | expected - ${false} | ${false} | ${false} | ${false} - ${true} | ${false} | ${false} | ${true} - ${true} | ${true} | ${false} | ${true} - ${true} | ${true} | ${true} | ${false} - `( - "should determine correctly if a WebAuthn credential should be registered", - async ({ isSupported, userHasCredential, credentialMatched, expected }) => { - jest.spyOn(WebauthnSupport, "supported").mockReturnValue(isSupported); - - const user: User = { - id: userID, - email: "", - webauthn_credentials: [], - }; - - if (userHasCredential) { - user.webauthn_credentials.push({ id: credentialID }); - } - - if (credentialMatched) { - jest - .spyOn(webauthnClient.webauthnState, "matchCredentials") - .mockReturnValueOnce([{ id: credentialID }]); - } else { - jest - .spyOn(webauthnClient.webauthnState, "matchCredentials") - .mockReturnValueOnce([]); - } - - const shouldRegister = await webauthnClient.shouldRegister(user); - - expect(WebauthnSupport.supported).toHaveBeenCalled(); - expect(shouldRegister).toEqual(expected); - } - ); -}); - -describe("webauthnClient._createAbortSignal()", () => { - it("should call abort() on the current controller and return a new one", async () => { - const signal1 = webauthnClient._createAbortSignal(); - const abortFn = jest.fn(); - webauthnClient.controller.abort = abortFn; - const signal2 = webauthnClient._createAbortSignal(); - expect(abortFn).toHaveBeenCalled(); - expect(signal1).not.toBe(signal2); - }); -}); - -describe("webauthnClient.listCredentials()", () => { - it("should list webauthn credentials", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - response._decodedJSON = [ - { - id: credentialID, - public_key: "", - attestation_type: "", - aaguid: "", - created_at: "", - transports: [], - }, - ]; - - jest.spyOn(webauthnClient.client, "get").mockResolvedValue(response); - const list = await webauthnClient.listCredentials(); - expect(webauthnClient.client.get).toHaveBeenCalledWith( - "/webauthn/credentials" - ); - expect(list).toEqual(response._decodedJSON); - }); - - it.each` - status | error - ${401} | ${"Unauthorized error"} - ${500} | ${"Technical error"} - `( - "should throw error if API returns an error status", - async ({ status, error }) => { - const response = new Response(new XMLHttpRequest()); - response.status = status; - response.ok = status >= 200 && status <= 299; - - jest.spyOn(webauthnClient.client, "get").mockResolvedValueOnce(response); - - const email = webauthnClient.listCredentials(); - await expect(email).rejects.toThrow(error); - } - ); - - it("should throw error on API communication failure", async () => { - webauthnClient.client.get = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const user = webauthnClient.listCredentials(); - await expect(user).rejects.toThrowError("Test error"); - }); -}); - -describe("webauthnClient.updateCredential()", () => { - it("should update a webauthn credential", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - - jest.spyOn(webauthnClient.client, "patch").mockResolvedValue(response); - const update = await webauthnClient.updateCredential( - credentialID, - "new name" - ); - expect(webauthnClient.client.patch).toHaveBeenCalledWith( - `/webauthn/credentials/${credentialID}`, - { name: "new name" } - ); - expect(update).toEqual(undefined); - }); - - it.each` - status | error - ${401} | ${"Unauthorized error"} - ${500} | ${"Technical error"} - `( - "should throw error if API returns an error status", - async ({ status, error }) => { - const response = new Response(new XMLHttpRequest()); - response.status = status; - response.ok = status >= 200 && status <= 299; - - jest - .spyOn(webauthnClient.client, "patch") - .mockResolvedValueOnce(response); - - const email = webauthnClient.updateCredential(credentialID, "new name"); - await expect(email).rejects.toThrow(error); - } - ); - - it("should throw error on API communication failure", async () => { - webauthnClient.client.patch = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const user = webauthnClient.updateCredential(credentialID, "new name"); - await expect(user).rejects.toThrowError("Test error"); - }); -}); - -describe("webauthnClient.delete()", () => { - it("should delete a webauthn credential", async () => { - const response = new Response(new XMLHttpRequest()); - response.ok = true; - - jest.spyOn(webauthnClient.client, "delete").mockResolvedValue(response); - const deleteResponse = await webauthnClient.deleteCredential(credentialID); - expect(webauthnClient.client.delete).toHaveBeenCalledWith( - `/webauthn/credentials/${credentialID}` - ); - expect(deleteResponse).toEqual(undefined); - }); - - it.each` - status | error - ${401} | ${"Unauthorized error"} - ${500} | ${"Technical error"} - `( - "should throw error if API returns an error status", - async ({ status, error }) => { - const response = new Response(new XMLHttpRequest()); - response.status = status; - response.ok = status >= 200 && status <= 299; - - jest - .spyOn(webauthnClient.client, "delete") - .mockResolvedValueOnce(response); - - const deleteResponse = webauthnClient.deleteCredential(credentialID); - await expect(deleteResponse).rejects.toThrow(error); - } - ); - - it("should throw error on API communication failure", async () => { - webauthnClient.client.delete = jest - .fn() - .mockRejectedValue(new Error("Test error")); - - const user = webauthnClient.deleteCredential(credentialID); - await expect(user).rejects.toThrowError("Test error"); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/events/Dispatcher.spec.ts b/frontend/frontend-sdk/tests/lib/events/Dispatcher.spec.ts index 62eee0c1c..343a45142 100644 --- a/frontend/frontend-sdk/tests/lib/events/Dispatcher.spec.ts +++ b/frontend/frontend-sdk/tests/lib/events/Dispatcher.spec.ts @@ -1,9 +1,5 @@ import { Dispatcher } from "../../../src/lib/events/Dispatcher"; -import { - AuthFlowCompletedDetail, - CustomEventWithDetail, - SessionDetail, -} from "../../../src"; +import { CustomEventWithDetail, SessionDetail } from "../../../src"; describe("Dispatcher", () => { let dispatcher: Dispatcher; @@ -25,7 +21,7 @@ describe("Dispatcher", () => { expect(dispatchEventSpy).toHaveBeenCalledTimes(1); expect(dispatchEventSpy).toHaveBeenCalledWith( - new CustomEventWithDetail("hanko-session-created", detail) + new CustomEventWithDetail("hanko-session-created", detail), ); const event = dispatchEventSpy.mock .calls[0][0] as CustomEventWithDetail; @@ -42,7 +38,7 @@ describe("Dispatcher", () => { expect(dispatchEventSpy).toHaveBeenCalledTimes(1); expect(dispatchEventSpy).toHaveBeenCalledWith( - new CustomEventWithDetail("hanko-session-expired", null) + new CustomEventWithDetail("hanko-session-expired", null), ); const event = dispatchEventSpy.mock .calls[0][0] as CustomEventWithDetail; @@ -58,7 +54,7 @@ describe("Dispatcher", () => { expect(dispatchEventSpy).toHaveBeenCalledTimes(1); expect(dispatchEventSpy).toHaveBeenCalledWith( - new CustomEventWithDetail("hanko-user-deleted", null) + new CustomEventWithDetail("hanko-user-deleted", null), ); const event = dispatchEventSpy.mock .calls[0][0] as CustomEventWithDetail; @@ -74,29 +70,11 @@ describe("Dispatcher", () => { expect(dispatchEventSpy).toHaveBeenCalledTimes(1); expect(dispatchEventSpy).toHaveBeenCalledWith( - new CustomEventWithDetail("hanko-user-logged-out", null) + new CustomEventWithDetail("hanko-user-logged-out", null), ); const event = dispatchEventSpy.mock .calls[0][0] as CustomEventWithDetail; expect(event.type).toEqual("hanko-user-logged-out"); }); }); - - describe("dispatchAuthFlowCompletedEvent()", () => { - it("dispatches a custom event with the 'hanko-auth-flow-completed' type and the provided detail", () => { - const detail = { userID: "test-user" }; - const dispatchEventSpy = jest.spyOn(dispatcher, "_dispatchEvent"); - - dispatcher.dispatchAuthFlowCompletedEvent(detail); - - expect(dispatchEventSpy).toHaveBeenCalledTimes(1); - expect(dispatchEventSpy).toHaveBeenCalledWith( - new CustomEventWithDetail("hanko-auth-flow-completed", detail) - ); - const event = dispatchEventSpy.mock - .calls[0][0] as CustomEventWithDetail; - expect(event.type).toEqual("hanko-auth-flow-completed"); - expect(event.detail).toBe(detail); - }); - }); }); diff --git a/frontend/frontend-sdk/tests/lib/events/Listener.spec.ts b/frontend/frontend-sdk/tests/lib/events/Listener.spec.ts index ee20f63cb..7fadd60d9 100644 --- a/frontend/frontend-sdk/tests/lib/events/Listener.spec.ts +++ b/frontend/frontend-sdk/tests/lib/events/Listener.spec.ts @@ -1,6 +1,5 @@ import { Listener } from "../../../src/lib/events/Listener"; import { - authFlowCompletedType, sessionCreatedType, sessionExpiredType, userDeletedType, @@ -39,7 +38,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( sessionCreatedType, expect.any(Function), - { once: false } + { once: false }, ); expect(mockThrottleFunc).toHaveBeenCalledWith( @@ -48,7 +47,7 @@ describe("Listener()", () => { { leading: true, trailing: false, - } + }, ); const mockEvent = new CustomEvent(sessionCreatedType, { @@ -80,7 +79,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( sessionCreatedType, expect.any(Function), - { once: true } + { once: true }, ); document.dispatchEvent(mockEvent); @@ -117,7 +116,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( sessionExpiredType, expect.any(Function), - { once: false } + { once: false }, ); expect(mockThrottleFunc).toHaveBeenCalledWith( @@ -126,7 +125,7 @@ describe("Listener()", () => { { leading: true, trailing: false, - } + }, ); const mockEvent = new CustomEvent(sessionExpiredType, {}); @@ -147,7 +146,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( sessionExpiredType, expect.any(Function), - { once: true } + { once: true }, ); document.dispatchEvent(mockEvent); @@ -169,75 +168,6 @@ describe("Listener()", () => { }); }); - describe("onAuthFlowCompleted()", () => { - it("should add an event listener for auth flow completed events", async () => { - const mockDetail = { - userID: "testUser", - }; - - listener.onAuthFlowCompleted(mockCallback); - - expect(addEventListenerSpy).toHaveBeenCalledWith( - authFlowCompletedType, - expect.any(Function), - { once: false } - ); - - expect(mockThrottleFunc).toBeCalledTimes(0); - - const mockEvent = new CustomEvent(authFlowCompletedType, { - detail: mockDetail, - }); - - document.dispatchEvent(mockEvent); - document.dispatchEvent(mockEvent); - document.dispatchEvent(mockEvent); - - expect(mockCallback).toHaveBeenCalledWith(mockDetail); - expect(mockCallback).toHaveBeenCalledTimes(3); - }); - - it("should only execute the callback once", async () => { - const mockDetail = { - userID: "testUser", - }; - - const mockEvent = new CustomEvent(authFlowCompletedType, { - detail: mockDetail, - }); - - listener.onAuthFlowCompleted(mockCallback, true); - - expect(addEventListenerSpy).toHaveBeenCalledWith( - authFlowCompletedType, - expect.any(Function), - { once: true } - ); - - document.dispatchEvent(mockEvent); - document.dispatchEvent(mockEvent); - - expect(mockCallback).toBeCalledTimes(1); - }); - - it("should clean up the event listener", async () => { - const mockDetail = { - userID: "testUser", - }; - - const mockEvent = new CustomEvent(authFlowCompletedType, { - detail: mockDetail, - }); - - const cleanup = listener.onAuthFlowCompleted(mockCallback, true); - - cleanup(); - - document.dispatchEvent(mockEvent); - expect(mockCallback).toBeCalledTimes(0); - }); - }); - describe("onUserLogged()", () => { it("should add an event listener for user logged out events", async () => { listener.onUserLoggedOut(mockCallback); @@ -245,7 +175,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( userLoggedOutType, expect.any(Function), - { once: false } + { once: false }, ); expect(mockThrottleFunc).toBeCalledTimes(0); @@ -267,7 +197,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( userLoggedOutType, expect.any(Function), - { once: true } + { once: true }, ); document.dispatchEvent(mockEvent); @@ -295,7 +225,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( userDeletedType, expect.any(Function), - { once: false } + { once: false }, ); expect(mockThrottleFunc).toBeCalledTimes(0); @@ -317,7 +247,7 @@ describe("Listener()", () => { expect(addEventListenerSpy).toHaveBeenCalledWith( userDeletedType, expect.any(Function), - { once: true } + { once: true }, ); document.dispatchEvent(mockEvent); diff --git a/frontend/frontend-sdk/tests/lib/events/Relay.spec.ts b/frontend/frontend-sdk/tests/lib/events/Relay.spec.ts index bc3e27c48..17be83349 100644 --- a/frontend/frontend-sdk/tests/lib/events/Relay.spec.ts +++ b/frontend/frontend-sdk/tests/lib/events/Relay.spec.ts @@ -28,11 +28,11 @@ describe("Relay", () => { }; const sessionCreatedEventMock = new CustomEventWithDetail( sessionCreatedType, - mockSessionCreatedDetail + mockSessionCreatedDetail, ); const sessionExpiredEventMock = new CustomEventWithDetail( sessionExpiredType, - null + null, ); document.dispatchEvent(sessionCreatedEventMock); @@ -61,11 +61,11 @@ describe("Relay", () => { }; const sessionCreatedEventMock = new CustomEventWithDetail( sessionCreatedType, - mockSessionCreatedDetail + mockSessionCreatedDetail, ); const userDeletedEventMock = new CustomEventWithDetail( userDeletedType, - null + null, ); document.dispatchEvent(sessionCreatedEventMock); @@ -88,7 +88,6 @@ describe("Relay", () => { }); it("should listen to 'storage' events and dispatch 'hanko-session-expired' if the session is expired", () => { - jest.spyOn(relay._session._sessionState, "getUserID").mockReturnValue(""); jest.spyOn(relay._session._cookie, "getAuthCookie").mockReturnValue(""); jest .spyOn(relay._session._sessionState, "getExpirationSeconds") @@ -97,7 +96,7 @@ describe("Relay", () => { window.dispatchEvent( new StorageEvent("storage", { key: "hanko_session", - }) + }), ); expect(dispatcherSpy).toHaveBeenCalled(); @@ -105,9 +104,6 @@ describe("Relay", () => { }); it("should listen to 'storage' events and dispatch 'hanko-session-created' if session is active", () => { - jest - .spyOn(relay._session._sessionState, "getUserID") - .mockReturnValue("test-user"); jest .spyOn(relay._session._cookie, "getAuthCookie") .mockReturnValue("test-jwt"); @@ -118,7 +114,7 @@ describe("Relay", () => { window.dispatchEvent( new StorageEvent("storage", { key: "hanko_session", - }) + }), ); expect(dispatcherSpy).toHaveBeenCalled(); diff --git a/frontend/frontend-sdk/tests/lib/state/PasscodeState.spec.ts b/frontend/frontend-sdk/tests/lib/state/PasscodeState.spec.ts deleted file mode 100644 index 84b4be62a..000000000 --- a/frontend/frontend-sdk/tests/lib/state/PasscodeState.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { decodedLSContent } from "../../setup"; -import { PasscodeState } from "../../../src/lib/state/users/PasscodeState"; - -describe("passcodeState.read()", () => { - it("should read the password state", async () => { - const state = new PasscodeState("hanko"); - - expect(state.read()).toEqual(state); - }); -}); - -describe("passcodeState.getActiveID()", () => { - it("should return the id of the currently active passcode", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = ls; - - expect(state.getActiveID(userID)).toEqual(ls.users[userID].passcode.id); - }); -}); - -describe("passcodeState.setActiveID()", () => { - it("should return the id of the currently active passcode", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - const passcodeID = "test_id_1"; - - state.ls = ls; - - expect(state.setActiveID(userID, passcodeID)).toEqual(state); - expect(state.ls.users[userID].passcode.id).toEqual(passcodeID); - }); -}); - -describe("passcodeState.reset()", () => { - it("should reset information about the active passcode", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = ls; - - expect(state.reset(userID)).toEqual(state); - expect(state.ls.users[userID].passcode.id).toBeUndefined(); - expect(state.ls.users[userID].passcode.ttl).toBeUndefined(); - expect(state.ls.users[userID].passcode.resendAfter).toBeUndefined(); - }); -}); - -describe("passcodeState.getResendAfter()", () => { - it("should return seconds until a new passcode can be send", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(decodedLSContent().users)[0]; - - state.ls = ls; - - expect(state.getResendAfter(userID)).toEqual(301); - }); -}); - -describe("passcodeState.setResendAfter()", () => { - it("should set a timestamp until a new passcode can be send", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - const seconds = 42; - - expect(state.setResendAfter(userID, seconds)).toEqual(state); - expect(state.ls.users[userID].passcode.resendAfter).toEqual( - Math.floor(Date.now() / 1000) + seconds - ); - }); -}); - -describe("passcodeState.getTTL()", () => { - it("should return seconds until the active passcode lives", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = ls; - - expect(state.getTTL(userID)).toEqual(301); - }); -}); - -describe("passcodeState.setTTL()", () => { - it("should set a timestamp until the active passcode lives", async () => { - const ls = decodedLSContent(); - const state = new PasscodeState("hanko"); - const userID = Object.keys(ls.users)[0]; - const seconds = 42; - - expect(state.setTTL(userID, seconds)).toEqual(state); - expect(state.ls.users[userID].passcode.ttl).toEqual( - Math.floor(Date.now() / 1000) + seconds - ); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/state/PasswordState.spec.ts b/frontend/frontend-sdk/tests/lib/state/PasswordState.spec.ts deleted file mode 100644 index 4abd9b8be..000000000 --- a/frontend/frontend-sdk/tests/lib/state/PasswordState.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { decodedLSContent } from "../../setup"; -import { PasswordState } from "../../../src/lib/state/users/PasswordState"; - -describe("passwordState.read()", () => { - it("should read the password state", async () => { - const state = new PasswordState("hanko"); - - expect(state.read()).toEqual(state); - }); -}); - -describe("passwordState.getRetryAfter()", () => { - it("should return seconds until a new login can be attempted", async () => { - const ls = decodedLSContent(); - const state = new PasswordState("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = ls; - - expect(state.getRetryAfter(userID)).toEqual(301); - }); -}); - -describe("passwordState.setRetryAfter()", () => { - it("should set a timestamp until a new login can be attempted", async () => { - const ls = decodedLSContent(); - const state = new PasswordState("hanko"); - const userID = Object.keys(ls.users)[0]; - const seconds = 42; - - expect(state.setRetryAfter(userID, seconds)).toEqual(state); - expect(state.ls.users[userID].password.retryAfter).toEqual( - Math.floor(Date.now() / 1000) + seconds - ); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/state/SessionState.spec.ts b/frontend/frontend-sdk/tests/lib/state/SessionState.spec.ts index c8895d4c9..af4eb20ff 100644 --- a/frontend/frontend-sdk/tests/lib/state/SessionState.spec.ts +++ b/frontend/frontend-sdk/tests/lib/state/SessionState.spec.ts @@ -9,30 +9,6 @@ describe("sessionState.read()", () => { }); }); -describe("sessionState.getUserID()", () => { - it("should return the user id", async () => { - const ls = decodedLSContent(); - const state = new SessionState({ localStorageKey: "hanko" }); - - state.ls = ls; - - expect(state.getUserID()).toEqual(ls.session.userID); - }); -}); - -describe("sessionState.setUserID()", () => { - it("should set the id of the current user", async () => { - const ls = decodedLSContent(); - const state = new SessionState({ localStorageKey: "hanko" }); - const userID = "test_id_1"; - - state.ls = ls; - - expect(state.setUserID(userID)).toEqual(state); - expect(state.ls.session.userID).toEqual(userID); - }); -}); - describe("sessionState.reset()", () => { it("should reset information about the current session", async () => { const ls = decodedLSContent(); @@ -41,7 +17,6 @@ describe("sessionState.reset()", () => { state.ls = ls; expect(state.reset()).toEqual(state); - expect(state.ls.session.userID).toBeUndefined(); expect(state.ls.session.expiry).toBeUndefined(); }); }); @@ -64,7 +39,7 @@ describe("sessionState.setExpirationSeconds()", () => { expect(state.setExpirationSeconds(seconds)).toEqual(state); expect(state.ls.session.expiry).toEqual( - Math.floor(Date.now() / 1000) + seconds + Math.floor(Date.now() / 1000) + seconds, ); }); }); diff --git a/frontend/frontend-sdk/tests/lib/state/UserState.spec.ts b/frontend/frontend-sdk/tests/lib/state/UserState.spec.ts deleted file mode 100644 index b56888c1f..000000000 --- a/frontend/frontend-sdk/tests/lib/state/UserState.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { decodedLSContent } from "../../setup"; -import { UserState } from "../../../src/lib/state/users/UserState"; - -describe("userState.getUserState()", () => { - it("should return the user state when local storage is initialized", async () => { - const ls = decodedLSContent(); - const state = new (class extends UserState {})("hanko"); - const userID = Object.keys(decodedLSContent().users)[0]; - - state.ls = decodedLSContent(); - expect(state.getUserState(userID)).toEqual(ls.users[userID]); - }); - - it("should return the user state when local storage is uninitialized", async () => { - const ls = decodedLSContent(); - const state = new (class extends UserState {})("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = {}; - expect(state.getUserState(userID)).toEqual({}); - }); -}); diff --git a/frontend/frontend-sdk/tests/lib/state/WebauthnState.spec.ts b/frontend/frontend-sdk/tests/lib/state/WebauthnState.spec.ts deleted file mode 100644 index 1216ffd92..000000000 --- a/frontend/frontend-sdk/tests/lib/state/WebauthnState.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { decodedLSContent } from "../../setup"; -import { WebauthnState } from "../../../src/lib/state/users/WebauthnState"; -import { Credential } from "../../../src"; - -describe("webauthnState.read()", () => { - it("should read the webauthn state", async () => { - const state = new WebauthnState("hanko"); - - expect(state.read()).toEqual(state); - }); -}); - -describe("webauthnState.getCredentials()", () => { - it("should read the webauthn state", async () => { - const ls = decodedLSContent(); - const state = new WebauthnState("hanko"); - const userID = Object.keys(ls.users)[0]; - - state.ls = ls; - expect(state.getCredentials(userID)).toEqual( - ls.users[userID].webauthn.credentials - ); - }); -}); - -describe("webauthnState.addCredential()", () => { - it("should add a credential id", async () => { - const ls = decodedLSContent(); - const state = new WebauthnState("hanko"); - const userID = Object.keys(ls.users)[0]; - const credentialID = "testCredentialID"; - - expect(state.addCredential(userID, credentialID)).toEqual(state); - expect(state.ls.users[userID].webauthn.credentials).toContainEqual( - credentialID - ); - }); -}); - -describe("webauthnState.matchCredentials()", () => { - it("should match credential ids", async () => { - const ls = decodedLSContent(); - const state = new WebauthnState("hanko"); - const userID = Object.keys(ls.users)[0]; - const credentials = ls.users[userID].webauthn.credentials.map( - (id) => ({ id } as Credential) - ); - const more = [{ id: "testCredentialID" } as Credential]; - - state.ls = ls; - - expect(state.matchCredentials(userID, credentials.concat(more))).toEqual( - credentials - ); - }); - - it("shouldn't match credential ids", async () => { - const ls = decodedLSContent(); - const state = new WebauthnState("hanko"); - const userID = Object.keys(ls.users)[0]; - const credentials = ls.users[userID].webauthn.credentials.map( - (id) => ({ id } as Credential) - ); - - state.ls = ls; - state.ls.users[userID].webauthn.credentials = ["testCredentialID"]; - - expect(state.matchCredentials(userID, credentials)).toEqual([]); - }); -}); diff --git a/frontend/frontend-sdk/tests/setup.ts b/frontend/frontend-sdk/tests/setup.ts index 5cea5c7f3..8c4f9d147 100644 --- a/frontend/frontend-sdk/tests/setup.ts +++ b/frontend/frontend-sdk/tests/setup.ts @@ -1,28 +1,10 @@ import { LocalStorage } from "../src/lib/state/State"; export const encodedLSContent = () => - "JTI1N0IlMjUyMnVzZXJzJTI1MjIlMjUzQSUyNTdCJTI1MjIxMzQ4ZDllNC1jNDE5LTQxNmEtYjMxZS1mMDUzOThhOGVhOWElMjUyMiUyNTNBJTI1N0IlMjUyMnBhc3Njb2RlJTI1MjIlMjUzQSUyNTdCJTI1MjJpZCUyNTIyJTI1M0ElMjUyMmIwNDVlZTZiLWE3OTAtNDcxZi1iMGViLTFkODYwZDJkYzYyNyUyNTIyJTI1MkMlMjUyMnR0bCUyNTIyJTI1M0ExNjY0MzgwMDAwJTI1MkMlMjUyMnJlc2VuZEFmdGVyJTI1MjIlMjUzQTE2NjQzODAwMDAlMjU3RCUyNTJDJTI1MjJwYXNzd29yZCUyNTIyJTI1M0ElMjU3QiUyNTIycmV0cnlBZnRlciUyNTIyJTI1M0ExNjY0MzgwMDAwJTI1N0QlMjUyQyUyNTIyd2ViYXV0aG4lMjUyMiUyNTNBJTI1N0IlMjUyMmNyZWRlbnRpYWxzJTI1MjIlMjUzQSUyNTVCJTI1MjI3bUZaM2VvSGNCcWpJaUFPRjMwc3VtVXNtcmhLUDhmV1dNdWxHcnhfdjkwZm5mQld2LTFIekFiaGVYWFg2MllwWmx1MG4zTnZoNGRqUlV5WFlvWEFmOXU0bWhaeGN2VFdxNkFzWHZKWEVRZVFEcmJHYVVUN29td0U4VktmRm5vJTI1MjIlMjU1RCUyNTdEJTI1N0QlMjU3RCUyNTJDJTI1MjJzZXNzaW9uJTI1MjIlMjUzQSUyNTdCJTI1MjJ1c2VySUQlMjUyMiUyNTNBJTI1MjJ0ZXN0LXVzZXIlMjUyMiUyNTJDJTI1MjJleHBpcnklMjUyMiUyNTNBMTY2NDM4MDAwMCUyNTJDJTI1MjJhdXRoRmxvd0NvbXBsZXRlZCUyNTIyJTI1M0FmYWxzZSUyNTdEJTI1N0Q="; + "JTI1N0IlMjUyMnNlc3Npb24lMjUyMiUyNTNBJTI1N0IlMjUyMmV4cGlyeSUyNTIyJTI1M0ExNjY0MzgwMDAwJTI1MkMlMjUyMmF1dGhGbG93Q29tcGxldGVkJTI1MjIlMjUzQWZhbHNlJTI1N0QlMjU3RA=="; export const decodedLSContent = (): LocalStorage => ({ - users: { - "1348d9e4-c419-416a-b31e-f05398a8ea9a": { - passcode: { - id: "b045ee6b-a790-471f-b0eb-1d860d2dc627", - ttl: 1664380000, - resendAfter: 1664380000, - }, - password: { - retryAfter: 1664380000, - }, - webauthn: { - credentials: [ - "7mFZ3eoHcBqjIiAOF30sumUsmrhKP8fWWMulGrx_v90fnfBWv-1HzAbheXXX62YpZlu0n3Nvh4djRUyXYoXAf9u4mhZxcvTWq6AsXvJXEQeQDrbGaUT7omwE8VKfFno", - ], - }, - }, - }, session: { - userID: "test-user", expiry: 1664380000, authFlowCompleted: false, }, @@ -82,7 +64,6 @@ export const fakeXMLHttpRequest = (function () { })); })(); - Object.defineProperty(global, "XMLHttpRequest", { value: fakeXMLHttpRequest, configurable: true, diff --git a/quickstart/public/html/index.html b/quickstart/public/html/index.html index 45f375177..7d7fdbc88 100644 --- a/quickstart/public/html/index.html +++ b/quickstart/public/html/index.html @@ -27,7 +27,7 @@ const { hanko } = await register("{{.HankoUrl}}"); - hanko.onAuthFlowCompleted(() => { + hanko.onSessionCreated(() => { document.location.href = "/secured" }) From 8b14945b5eca4fda881ef57fa80da7c6aaaab9f7 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Tue, 6 Aug 2024 10:36:03 +0200 Subject: [PATCH 277/278] chore: remove unused files --- ...fig.ts.timestamp-1722018527125-6ce80a3ff5998.mjs | 13 ------------- ...fig.ts.timestamp-1722018585581-54c54243ae1b1.mjs | 13 ------------- ...fig.ts.timestamp-1722018601662-b24bdba6162a1.mjs | 13 ------------- ...fig.ts.timestamp-1722018673039-8a928d5ea58a5.mjs | 13 ------------- ...fig.ts.timestamp-1722018705277-a517541ebadfa.mjs | 13 ------------- ...fig.ts.timestamp-1722019069953-b82880cdb3a0c.mjs | 13 ------------- ...fig.ts.timestamp-1722019120658-9d1b63c64ae15.mjs | 13 ------------- ...nfig.ts.timestamp-1722019137148-da04f0eb2a35.mjs | 13 ------------- ...fig.ts.timestamp-1722019164606-cfd941bef9df8.mjs | 13 ------------- ...fig.ts.timestamp-1722019295090-b580662a1aa9c.mjs | 13 ------------- ...fig.ts.timestamp-1722019354757-4855cab69cada.mjs | 13 ------------- ...fig.ts.timestamp-1722019617815-166bf34be37e3.mjs | 13 ------------- ...fig.ts.timestamp-1722268706418-f7c9e7a290845.mjs | 13 ------------- ...fig.ts.timestamp-1722268741885-f7ef2bb91c705.mjs | 13 ------------- ...fig.ts.timestamp-1722268767938-27cad72e40222.mjs | 13 ------------- ...fig.ts.timestamp-1722268794889-04697cf7edb82.mjs | 13 ------------- 16 files changed, 208 deletions(-) delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs delete mode 100644 frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722018527125-6ce80a3ff5998.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722018585581-54c54243ae1b1.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722018601662-b24bdba6162a1.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722018673039-8a928d5ea58a5.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722018705277-a517541ebadfa.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019069953-b82880cdb3a0c.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019120658-9d1b63c64ae15.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019137148-da04f0eb2a35.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019164606-cfd941bef9df8.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019295090-b580662a1aa9c.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019354757-4855cab69cada.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722019617815-166bf34be37e3.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722268706418-f7c9e7a290845.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722268741885-f7ef2bb91c705.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722268767938-27cad72e40222.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs b/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs deleted file mode 100644 index 6d3e76e8c..000000000 --- a/frontend/examples/svelte/vite.config.ts.timestamp-1722268794889-04697cf7edb82.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/vite/dist/node/index.js"; -import { svelte } from "file:///Users/bm/GolandProjects/hanko/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -var vite_config_default = defineConfig({ - server: { - host: "0.0.0.0" - }, - plugins: [svelte()] -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYm0vR29sYW5kUHJvamVjdHMvaGFua28vZnJvbnRlbmQvZXhhbXBsZXMvc3ZlbHRlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ibS9Hb2xhbmRQcm9qZWN0cy9oYW5rby9mcm9udGVuZC9leGFtcGxlcy9zdmVsdGUvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSAnQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZSdcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHNlcnZlcjoge1xuICAgIGhvc3Q6ICcwLjAuMC4wJ1xuICB9LFxuICBwbHVnaW5zOiBbc3ZlbHRlKCldXG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF1VixTQUFTLG9CQUFvQjtBQUNwWCxTQUFTLGNBQWM7QUFHdkIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K From 795621fe00dad1fab351e3f7b7d7e87af309dac9 Mon Sep 17 00:00:00 2001 From: bjoern-m Date: Tue, 6 Aug 2024 15:44:18 +0200 Subject: [PATCH 278/278] chore: remove another dynamic import of hanko-elements --- frontend/examples/nextjs/pages/todo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/examples/nextjs/pages/todo.tsx b/frontend/examples/nextjs/pages/todo.tsx index cc46a6de3..231afa5b0 100644 --- a/frontend/examples/nextjs/pages/todo.tsx +++ b/frontend/examples/nextjs/pages/todo.tsx @@ -21,7 +21,7 @@ const Todo: NextPage = () => { const modalRef = useRef(null); useEffect(() => { - import("@teamhanko/hanko-elements").then(({ Hanko }) => setHankoClient(new Hanko(hankoAPI))); + setHankoClient(new Hanko(hankoAPI)); }, []); const redirectToProfile = () => {