Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package flow
|
|
|
|
import (
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Flow State
|
|
//
|
|
// The state represents the state of the verification flow.
|
|
//
|
|
// - choose_method: ask the user to choose a method (e.g. recover account via email)
|
|
// - sent_email: the email has been sent to the user
|
|
// - passed_challenge: the request was successful and the recovery challenge was passed.
|
|
// - show_form: a form is shown to the user to perform the flow
|
|
// - success: the flow has been completed successfully
|
|
//
|
|
// swagger:enum selfServiceFlowState
|
|
type State string
|
|
|
|
// #nosec G101 -- only a key constant
|
|
// Define the various states for all flows.
|
|
// Recovery flows for V2 have different states (see below).
|
|
const (
|
|
StateChooseMethod State = "choose_method"
|
|
// Note: this state should actually be called `StateMessageSent`,
|
|
// where a 'Message' is a code or link sent to an address (e.g. `email`, `sms`, etc).
|
|
StateEmailSent State = "sent_email"
|
|
StatePassedChallenge State = "passed_challenge"
|
|
StateShowForm State = "show_form"
|
|
StateSuccess State = "success"
|
|
|
|
// Recovery V2.
|
|
// The initial state is different from `StateChooseMethod` to distinguish recovery v1 vs v2.
|
|
// This avoids the issue of the feature flag being toggled while some recovery flows are on-going.
|
|
// This would lead to an inconsistent state machine/logic for these flows.
|
|
StateRecoveryAwaitingAddress State = "recovery_awaiting_address"
|
|
StateRecoveryAwaitingAddressChoice State = "recovery_awaiting_address_choice"
|
|
StateRecoveryAwaitingAddressConfirm State = "recovery_confirming_address"
|
|
StateRecoveryAwaitingCode State = "recovery_awaiting_code"
|
|
// The final success state is the same as in Recovery V1 (`passed_challenge`).
|
|
// Since this is the terminal state, it is not affected by toggling the feature flag.
|
|
|
|
// State machine diagrams:
|
|
// - ./state_recovery_v1.mermaid
|
|
// - ./state_recovery_v2.mermaid
|
|
)
|
|
|
|
var states = []State{
|
|
StateChooseMethod,
|
|
StateEmailSent,
|
|
StatePassedChallenge,
|
|
}
|
|
|
|
func indexOf(current State) int {
|
|
for k, s := range states {
|
|
if s == current {
|
|
return k
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func HasReachedState(expected, actual State) bool {
|
|
return indexOf(actual) >= indexOf(expected)
|
|
}
|
|
|
|
func IsStateRecoveryV2(state State) bool {
|
|
return strings.HasPrefix(state.String(), "recovery_")
|
|
}
|
|
|
|
func NextState(current State) State {
|
|
if current == StatePassedChallenge {
|
|
return StatePassedChallenge
|
|
}
|
|
|
|
return states[indexOf(current)+1]
|
|
}
|
|
|
|
// For some reason using sqlxx.NullString as the State type does not work here.
|
|
// Reimplementing the Scanner interface on type State does work and allows
|
|
// the state to be NULL in the database.
|
|
|
|
// MarshalJSON returns m as the JSON encoding of m.
|
|
func (ns State) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(string(ns))
|
|
}
|
|
|
|
// UnmarshalJSON sets *m to a copy of data.
|
|
func (ns *State) UnmarshalJSON(data []byte) error {
|
|
if ns == nil {
|
|
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
|
}
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
return errors.WithStack(json.Unmarshal(data, (*string)(ns)))
|
|
}
|
|
|
|
// Scan implements the Scanner interface.
|
|
func (ns *State) Scan(value interface{}) error {
|
|
var v sql.NullString
|
|
if err := (&v).Scan(value); err != nil {
|
|
return err
|
|
}
|
|
*ns = State(v.String)
|
|
return nil
|
|
}
|
|
|
|
// Value implements the driver Valuer interface.
|
|
func (ns State) Value() (driver.Value, error) {
|
|
if len(ns) == 0 {
|
|
return sql.NullString{}.Value()
|
|
}
|
|
return sql.NullString{Valid: true, String: string(ns)}.Value()
|
|
}
|
|
|
|
// String implements the Stringer interface.
|
|
func (ns State) String() string {
|
|
return string(ns)
|
|
}
|