Flow split begin
This commit is contained in:
parent
ae00c1534d
commit
f3e17e149f
10 changed files with 286 additions and 157 deletions
|
@ -15,6 +15,13 @@ type cmdOptions interface {
|
||||||
Execute() error
|
Execute() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AppNamePtr() *string {
|
||||||
|
s := AppName
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntPtr(i int) *int { return &i }
|
||||||
|
|
||||||
// RunE is a convenience function for use with cobra.
|
// RunE is a convenience function for use with cobra.
|
||||||
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
type (
|
type (
|
||||||
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
|
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
|
||||||
PyTimestamp time.Time
|
PyTimestamp time.Time
|
||||||
|
ClientID string
|
||||||
)
|
)
|
||||||
|
|
||||||
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"
|
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"
|
||||||
|
|
|
@ -24,7 +24,7 @@ var (
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
store AuthStore
|
store AuthStore
|
||||||
flows FlowStore
|
flows *AuthFlowManager
|
||||||
sessions AccessSessionStore
|
sessions AccessSessionStore
|
||||||
providers map[string]provider.AuthProvider
|
providers map[string]provider.AuthProvider
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
a.providers[nProv.ProviderType()] = nProv
|
a.providers[nProv.ProviderType()] = nProv
|
||||||
}
|
}
|
||||||
|
|
||||||
a.flows = make(FlowStore)
|
a.flows = NewAuthFlowManager()
|
||||||
|
|
||||||
a.sessions.init()
|
a.sessions.init()
|
||||||
|
|
||||||
|
@ -91,19 +91,14 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, providers)
|
return c.JSON(http.StatusOK, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
|
func (a *Authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
|
||||||
cID, hasCID := rm["client_id"]
|
cID, hasCID := rm["client_id"]
|
||||||
cIDStr, cidIsStr := cID.(string)
|
cIDStr, cidIsStr := cID.(string)
|
||||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
|
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) {
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range f.Handler {
|
p := a.Provider(f.Handler.String())
|
||||||
if h == nil {
|
|
||||||
return nil, ErrInvalidHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
p := a.Provider(*h)
|
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
@ -114,7 +109,6 @@ func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interfac
|
||||||
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
165
pkg/auth/flow.go
165
pkg/auth/flow.go
|
@ -3,132 +3,67 @@ package auth
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/internal/generate"
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FlowStore map[FlowID]*Flow
|
type AuthFlowManager struct {
|
||||||
|
*flow.FlowManager
|
||||||
type FlowRequest struct {
|
|
||||||
ClientID ClientID `json:"client_id"`
|
|
||||||
Handler []*string `json:"handler"`
|
|
||||||
RedirectURI string `json:"redirect_uri"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlowType string
|
type LoginFlow struct {
|
||||||
|
flow.FlowHandlerBase
|
||||||
const (
|
|
||||||
TypeForm FlowType = "form"
|
|
||||||
TypeCreateEntry FlowType = "create_entry"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FlowID string
|
|
||||||
type Step string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StepInit Step = "init"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Flow struct {
|
|
||||||
Type FlowType `json:"type"`
|
|
||||||
ID FlowID `json:"flow_id"`
|
|
||||||
Handler []*string `json:"handler"`
|
|
||||||
StepID *Step `json:"step_id,omitempty"`
|
|
||||||
Schema []provider.FlowSchemaItem `json:"data_schema"`
|
|
||||||
Errors interface{} `json:"errors"`
|
|
||||||
DescPlace *string `json:"description_placeholders"`
|
|
||||||
LastStep *string `json:"last_step"`
|
|
||||||
|
|
||||||
request *FlowRequest
|
|
||||||
ctime time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) touch() {
|
func NewAuthFlowManager() *AuthFlowManager {
|
||||||
f.ctime = time.Now()
|
return &AuthFlowManager{FlowManager: flow.NewFlowManager()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs FlowStore) register(f *Flow) {
|
func (afm *AuthFlowManager) NewLoginFlow(f *flow.FlowRequest, prov provider.AuthProvider) *LoginFlow {
|
||||||
fs.cull()
|
lf := &LoginFlow{
|
||||||
fs[f.ID] = f
|
FlowHandlerBase: flow.NewFlowHandlerBase(f, prov, prov.ProviderType()),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs FlowStore) Remove(f *Flow) {
|
afm.Register(lf)
|
||||||
delete(fs, f.ID)
|
|
||||||
|
return lf
|
||||||
}
|
}
|
||||||
|
|
||||||
const cullAge = time.Minute * 30
|
func (a *Authenticator) NewFlow(r *flow.FlowRequest) *flow.FlowResult {
|
||||||
|
var prov provider.AuthProvider
|
||||||
func (fs FlowStore) cull() {
|
|
||||||
for k, v := range fs {
|
|
||||||
if time.Now().Sub(v.ctime) > cullAge {
|
|
||||||
delete(fs, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs FlowStore) Get(id FlowID) *Flow {
|
|
||||||
f, ok := fs[id]
|
|
||||||
if ok {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
|
||||||
var sch []provider.FlowSchemaItem
|
|
||||||
|
|
||||||
for _, h := range r.Handler {
|
for _, h := range r.Handler {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if hand := a.Provider(*h); hand != nil {
|
prov = a.Provider(h.String())
|
||||||
sch = hand.FlowSchema()
|
if prov != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sch == nil {
|
if prov == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flow := &Flow{
|
flow := a.flows.NewLoginFlow(r, prov)
|
||||||
Type: TypeForm,
|
|
||||||
ID: FlowID(generate.UUID()),
|
|
||||||
StepID: stepPtr(StepInit),
|
|
||||||
Schema: sch,
|
|
||||||
Handler: r.Handler,
|
|
||||||
Errors: []string{},
|
|
||||||
request: r,
|
|
||||||
}
|
|
||||||
flow.touch()
|
|
||||||
|
|
||||||
a.flows.register(flow)
|
return flow.ShowForm(nil)
|
||||||
|
|
||||||
return flow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepPtr(s Step) *Step { return &s }
|
func (f *LoginFlow) redirect(c echo.Context) {
|
||||||
|
c.Request().Header.Set("Location", f.RedirectURI)
|
||||||
func (f *Flow) redirect(c echo.Context) {
|
|
||||||
c.Request().Header.Set("Location", f.request.RedirectURI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||||
if f.StepID == nil {
|
switch f.Step() {
|
||||||
c.Logger().Error("stepID is nil")
|
case flow.StepInit:
|
||||||
return c.String(http.StatusInternalServerError, "No Step ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch *f.StepID {
|
|
||||||
case StepInit:
|
|
||||||
rm := make(map[string]interface{})
|
rm := make(map[string]interface{})
|
||||||
|
|
||||||
err := c.Bind(&rm)
|
err := c.Bind(&rm)
|
||||||
|
@ -136,31 +71,21 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
return c.String(http.StatusBadRequest, err.Error())
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, si := range f.Schema {
|
err = f.Schema.CheckRequired(rm)
|
||||||
if si.Required {
|
if err != nil {
|
||||||
if _, ok := rm[si.Name]; !ok {
|
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
||||||
return c.String(http.StatusBadRequest, "missing required param "+si.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.Check(f, c.Request(), rm)
|
user, err := a.Check(f, c.Request(), rm)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
var finishedFlow struct {
|
finishedFlow := flow.FlowResult{}
|
||||||
ID FlowID `json:"flow_id"`
|
|
||||||
Handler []*string `json:"handler"`
|
|
||||||
Result AccessTokenID `json:"result"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Type FlowType `json:"type"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
a.flows.Remove(f)
|
a.flows.Remove(f)
|
||||||
copier.Copy(&finishedFlow, f)
|
copier.Copy(&finishedFlow, f)
|
||||||
finishedFlow.Type = TypeCreateEntry
|
finishedFlow.Type = flow.TypeCreateEntry
|
||||||
finishedFlow.Title = common.AppName
|
finishedFlow.Title = common.AppNamePtr()
|
||||||
finishedFlow.Version = 1
|
finishedFlow.Version = common.IntPtr(1)
|
||||||
finishedFlow.Result = a.NewAccessToken(c.Request(), user, f)
|
finishedFlow.Result = a.NewAccessToken(c.Request(), user)
|
||||||
|
|
||||||
f.redirect(c)
|
f.redirect(c)
|
||||||
|
|
||||||
|
@ -170,10 +95,10 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
case ErrInvalidAuth:
|
case ErrInvalidAuth:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
f.Errors = map[string]interface{}{
|
fr := f.ShowForm(map[string]interface{}{
|
||||||
"base": "invalid_auth",
|
"base": "invalid_auth",
|
||||||
}
|
})
|
||||||
return c.JSON(http.StatusOK, f)
|
return c.JSON(http.StatusOK, fr)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return c.String(http.StatusBadRequest, "unknown flow step")
|
return c.String(http.StatusBadRequest, "unknown flow step")
|
||||||
|
@ -181,13 +106,13 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
||||||
flowID := c.Param("flow_id")
|
flowID := flow.FlowID(c.Param("flow_id"))
|
||||||
|
|
||||||
if flowID == "" {
|
if flowID == "" {
|
||||||
return c.String(http.StatusBadRequest, "empty flow ID")
|
return c.String(http.StatusBadRequest, "empty flow ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(a.flows, FlowID(flowID))
|
a.flows.Delete(flowID)
|
||||||
|
|
||||||
return c.String(http.StatusOK, "deleted")
|
return c.String(http.StatusOK, "deleted")
|
||||||
}
|
}
|
||||||
|
@ -202,7 +127,7 @@ func setJSON(c echo.Context) {
|
||||||
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
||||||
setJSON(c)
|
setJSON(c)
|
||||||
|
|
||||||
var flowReq FlowRequest
|
var flowReq flow.FlowRequest
|
||||||
err := c.Bind(&flowReq)
|
err := c.Bind(&flowReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.String(http.StatusBadRequest, err.Error())
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
@ -222,16 +147,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
|
|
||||||
flowID := c.Param("flow_id")
|
flowID := c.Param("flow_id")
|
||||||
|
|
||||||
flow := a.flows.Get(FlowID(flowID))
|
flow := a.flows.Get(flow.FlowID(flowID))
|
||||||
if flow == nil {
|
if flow == nil {
|
||||||
return c.String(http.StatusNotFound, "no such flow")
|
return c.String(http.StatusNotFound, "no such flow")
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().Sub(flow.ctime) > cullAge {
|
return flow.(*LoginFlow).progress(a, c)
|
||||||
a.flows.Remove(flow)
|
|
||||||
|
|
||||||
return c.String(http.StatusGone, "flow timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
return flow.progress(a, c)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -126,8 +127,8 @@ func (hap *HomeAssistantProvider) NewCredData() interface{} {
|
||||||
return &HAUser{}
|
return &HAUser{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem {
|
func (hap *HomeAssistantProvider) FlowSchema() flow.FlowSchema {
|
||||||
return []provider.FlowSchemaItem{
|
return []flow.FlowSchemaItem{
|
||||||
{
|
{
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package provider
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ var Providers = make(map[string]Constructor)
|
||||||
type AuthProvider interface { // TODO: this should include stepping
|
type AuthProvider interface { // TODO: this should include stepping
|
||||||
AuthProviderMetadata
|
AuthProviderMetadata
|
||||||
ProviderBase() AuthProviderBase
|
ProviderBase() AuthProviderBase
|
||||||
FlowSchema() []FlowSchemaItem
|
FlowSchema() flow.FlowSchema
|
||||||
NewCredData() interface{}
|
NewCredData() interface{}
|
||||||
ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool)
|
ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool)
|
||||||
Lookup(ProviderUser) ProviderUser
|
Lookup(ProviderUser) ProviderUser
|
||||||
|
@ -24,7 +25,7 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderUser interface {
|
type ProviderUser interface {
|
||||||
// TODO: make sure this is sane with all the ProviderUser and UserData type stuf
|
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
|
||||||
UserData() ProviderUser
|
UserData() ProviderUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,9 +45,3 @@ func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
||||||
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
||||||
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
||||||
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
||||||
|
|
||||||
type FlowSchemaItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/flow"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,8 +62,8 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} {
|
||||||
return &UserData{}
|
return &UserData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *TrustedNetworksProvider) FlowSchema() []provider.FlowSchemaItem {
|
func (hap *TrustedNetworksProvider) FlowSchema() flow.FlowSchema {
|
||||||
return []provider.FlowSchemaItem{
|
return []flow.FlowSchemaItem{
|
||||||
{
|
{
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -131,7 +131,7 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request
|
||||||
|
|
||||||
const defaultExpiration = 15 * time.Minute
|
const defaultExpiration = 15 * time.Minute
|
||||||
|
|
||||||
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser, f *Flow) AccessTokenID {
|
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser) AccessTokenID {
|
||||||
id := AccessTokenID(generate.UUID())
|
id := AccessTokenID(generate.UUID())
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ const (
|
||||||
GTRefreshToken GrantType = "refresh_token"
|
GTRefreshToken GrantType = "refresh_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientID string
|
type ClientID common.ClientID
|
||||||
|
|
||||||
func (c *ClientID) IsValid() bool {
|
func (c *ClientID) IsValid() bool {
|
||||||
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
||||||
|
|
211
pkg/flow/flow.go
Normal file
211
pkg/flow/flow.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
// flow is the data entry flow.
|
||||||
|
package flow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
|
"dynatron.me/x/blasphem/internal/generate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FlowResultType string
|
||||||
|
FlowID string
|
||||||
|
Step string
|
||||||
|
HandlerKey string
|
||||||
|
Errors interface{}
|
||||||
|
|
||||||
|
Context interface{}
|
||||||
|
|
||||||
|
FlowStore map[FlowID]Handler
|
||||||
|
|
||||||
|
FlowManager struct {
|
||||||
|
flows FlowStore
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowResult struct {
|
||||||
|
Type FlowResultType `json:"type"`
|
||||||
|
ID FlowID `json:"flow_id"`
|
||||||
|
Handler []*HandlerKey `json:"handler"`
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
StepID *Step `json:"step_id,omitempty"`
|
||||||
|
Schema []FlowSchemaItem `json:"data_schema"`
|
||||||
|
Extra *string `json:"extra,omitempty"`
|
||||||
|
Required *bool `json:"required,omitempty"`
|
||||||
|
Errors interface{} `json:"errors"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DescPlace *string `json:"description_placeholders"`
|
||||||
|
URL *string `json:"url,omitempty"`
|
||||||
|
Reason *string `json:"reason,omitempty"`
|
||||||
|
Context *string `json:"context,omitempty"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
LastStep *string `json:"last_step"`
|
||||||
|
Options map[string]interface{} `json:"options,omitempty"`
|
||||||
|
Version *int `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowSchemaItem struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowSchema []FlowSchemaItem
|
||||||
|
|
||||||
|
FlowRequest struct {
|
||||||
|
ClientID common.ClientID `json:"client_id"`
|
||||||
|
Handler []*HandlerKey `json:"handler"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Schemer interface {
|
||||||
|
FlowSchema() FlowSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler interface {
|
||||||
|
Base() FlowHandlerBase
|
||||||
|
FlowID() FlowID
|
||||||
|
flowCtime() time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StepInit Step = "init"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (fs *FlowSchema) CheckRequired(rm map[string]interface{}) error {
|
||||||
|
for _, si := range *fs {
|
||||||
|
if si.Required {
|
||||||
|
if _, ok := rm[si.Name]; !ok {
|
||||||
|
return fmt.Errorf("missing required param %s", si.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlowManager() *FlowManager {
|
||||||
|
return &FlowManager{
|
||||||
|
flows: make(FlowStore),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stepPtr(s Step) *Step { return &s }
|
||||||
|
|
||||||
|
type FlowHandlerBase struct {
|
||||||
|
ID FlowID // ID is the FlowID
|
||||||
|
Handler HandlerKey // Handler key
|
||||||
|
Context Context // flow Context
|
||||||
|
ClientID common.ClientID
|
||||||
|
RedirectURI string
|
||||||
|
Schema FlowSchema
|
||||||
|
|
||||||
|
// curStep is the current step set by the flow manager
|
||||||
|
curStep Step
|
||||||
|
|
||||||
|
ctime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FlowHandlerBase) Step() Step { return f.curStep }
|
||||||
|
|
||||||
|
func (f *FlowHandlerBase) Base() FlowHandlerBase { return *f }
|
||||||
|
|
||||||
|
func (f *FlowHandlerBase) FlowID() FlowID {
|
||||||
|
return f.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FlowHandlerBase) flowCtime() time.Time { return f.ctime }
|
||||||
|
|
||||||
|
func NewFlowHandlerBase(f *FlowRequest, sch Schemer, hand string) FlowHandlerBase {
|
||||||
|
return FlowHandlerBase{
|
||||||
|
ID: FlowID(generate.UUID()),
|
||||||
|
Handler: HandlerKey(hand),
|
||||||
|
ClientID: f.ClientID,
|
||||||
|
RedirectURI: f.RedirectURI,
|
||||||
|
Schema: sch.FlowSchema(),
|
||||||
|
|
||||||
|
curStep: StepInit,
|
||||||
|
ctime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk *HandlerKey) String() string {
|
||||||
|
return string(*hk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowHandlerBase) Handlers() []*HandlerKey {
|
||||||
|
return []*HandlerKey{&fm.Handler, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultErrs(e Errors) Errors {
|
||||||
|
if e == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowHandlerBase) ShowForm(errs Errors) *FlowResult {
|
||||||
|
res := &FlowResult{
|
||||||
|
Type: TypeForm,
|
||||||
|
ID: fm.ID,
|
||||||
|
StepID: stepPtr(fm.curStep),
|
||||||
|
Schema: fm.Schema,
|
||||||
|
Handler: fm.Handlers(),
|
||||||
|
Errors: resultErrs(errs),
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowManager) Delete(id FlowID) {
|
||||||
|
delete(fm.flows, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeForm FlowResultType = "form"
|
||||||
|
TypeCreateEntry FlowResultType = "create_entry"
|
||||||
|
TypeAbort FlowResultType = "abort"
|
||||||
|
TypeExternalStep FlowResultType = "external"
|
||||||
|
TypeExternalStepDone FlowResultType = "external_done"
|
||||||
|
TypeShowProgress FlowResultType = "progress"
|
||||||
|
TypeShowProgressDone FlowResultType = "progress_done"
|
||||||
|
TypeMenu FlowResultType = "menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *FlowHandlerBase) touch() {
|
||||||
|
f.ctime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FlowManager) Register(f Handler) {
|
||||||
|
fm.flows.cull()
|
||||||
|
fm.flows[f.FlowID()] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlowManager) Remove(f Handler) {
|
||||||
|
delete(fs.flows, f.FlowID())
|
||||||
|
}
|
||||||
|
|
||||||
|
const cullAge = time.Minute * 30
|
||||||
|
|
||||||
|
func (fs FlowStore) cull() {
|
||||||
|
for k, v := range fs {
|
||||||
|
if time.Now().Sub(v.flowCtime()) > cullAge {
|
||||||
|
delete(fs, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlowManager) Get(id FlowID) Handler {
|
||||||
|
f, ok := fs.flows[id]
|
||||||
|
if ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue